├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ ├── application.js │ │ └── vendor │ │ │ ├── backbone.js │ │ │ └── underscore.js │ └── stylesheets │ │ └── start.css.scss ├── controllers │ ├── application_controller.rb │ ├── bands_controller.rb │ └── home_controller.rb ├── helpers │ ├── application_helper.rb │ └── start_helper.rb ├── models │ ├── access.rb │ ├── address.rb │ ├── album.rb │ ├── band.rb │ ├── following.rb │ ├── message.rb │ ├── message_report.rb │ ├── metadata.rb │ ├── pet.rb │ ├── photo.rb │ ├── reference │ │ └── role.rb │ ├── review.rb │ ├── role.rb │ ├── settings.rb │ ├── show.rb │ ├── tour.rb │ ├── track.rb │ ├── user.rb │ └── venue.rb ├── services │ ├── facebook.rb │ └── twitter.rb ├── uploaders │ └── image_uploader.rb └── views │ ├── home │ └── index.html.haml │ └── layouts │ └── application.html.haml ├── bin ├── bundle ├── rails └── rake ├── config.ru ├── config ├── application.rb ├── boot.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── secret_token.rb │ └── session_store.rb ├── locales │ ├── de.yml │ └── en.yml ├── mongoid.yml └── routes.rb ├── db ├── evolver │ └── migrations │ │ ├── 20120729133305_add_dm.rb │ │ ├── 20120729134130_add_tool.rb │ │ └── 20120729134137_add_placebo.rb └── seeds.rb ├── lib └── tasks │ └── .gitkeep ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── images │ └── rails.png └── robots.txt ├── script └── rails ├── spec ├── controllers │ └── home_controller_spec.rb ├── fabricators │ ├── access_fabricator.rb │ ├── address_fabricator.rb │ ├── album_fabricator.rb │ ├── band_fabricator.rb │ ├── following_fabricator.rb │ ├── metadata_fabricator.rb │ ├── photo_fabricator.rb │ ├── review_fabricator.rb │ ├── role_fabricator.rb │ ├── settings_fabricator.rb │ ├── show_fabricator.rb │ ├── tour_fabricator.rb │ ├── track_fabricator.rb │ ├── user_fabricator.rb │ └── venue_fabricator.rb ├── helpers │ └── start_helper_spec.rb ├── models │ ├── access_spec.rb │ ├── address_spec.rb │ ├── album_spec.rb │ ├── band_spec.rb │ ├── following_spec.rb │ ├── message_spec.rb │ ├── metadata_spec.rb │ ├── photo_spec.rb │ ├── reference │ │ └── role_spec.rb │ ├── review_spec.rb │ ├── role_spec.rb │ ├── show_spec.rb │ ├── tour_spec.rb │ ├── track_spec.rb │ ├── user_spec.rb │ └── venue_spec.rb ├── spec_helper.rb └── support │ └── bands │ └── depeche-mode │ ├── depeche-mode-table.jpg │ ├── depeche-mode-universe.jpg │ ├── depeche-mode-wall.jpg │ ├── singles-86-98-back.jpg │ └── singles-86-98-cover.jpg └── vendor └── plugins └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/ 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --drb 3 | --format documentation 4 | --fail-fast 5 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | echo 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without development 3 | matrix: 4 | include: 5 | - rvm: 1.9.3 6 | - rvm: ruby-head 7 | - rvm: jruby-19mode 8 | jdk: oraclejdk7 9 | env: JRUBY_OPTS="-Xmx512m -Xcompile.invokedynamic=false" 10 | - rvm: jruby-19mode 11 | jdk: openjdk7 12 | env: JRUBY_OPTS="-Xmx512m -Xcompile.invokedynamic=false" 13 | - rvm: jruby-head 14 | jdk: oraclejdk7 15 | env: JRUBY_OPTS="-Xcompile.invokedynamic=false" 16 | - rvm: jruby-head 17 | jdk: openjdk7 18 | env: JRUBY_OPTS="-Xcompile.invokedynamic=false" 19 | allow_failures: 20 | - rvm: ruby-head 21 | - rvm: jruby-head 22 | jdk: oraclejdk7 23 | env: JRUBY_OPTS="-Xcompile.invokedynamic=false" 24 | - rvm: jruby-head 25 | jdk: openjdk7 26 | env: JRUBY_OPTS="-Xcompile.invokedynamic=false" 27 | services: 28 | - mongodb 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'rails', "~> 4.2" 4 | 5 | gem "carrierwave" 6 | gem "carrierwave-mongoid", require: "carrierwave/mongoid" 7 | gem "decent_exposure" 8 | 9 | gem "mongoid", "~> 5.0.0.beta" 10 | 11 | gem 'mimetype-fu', require: 'mimetype_fu' 12 | gem 'responders' 13 | 14 | group :development, :test do 15 | gem "fabrication" 16 | gem "guard-rspec" 17 | gem "rspec-rails" 18 | end 19 | 20 | platforms :jruby do 21 | gem "jruby-openssl" 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.3) 5 | actionpack (= 4.2.3) 6 | actionview (= 4.2.3) 7 | activejob (= 4.2.3) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.3) 11 | actionview (= 4.2.3) 12 | activesupport (= 4.2.3) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.3) 18 | activesupport (= 4.2.3) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 23 | activejob (4.2.3) 24 | activesupport (= 4.2.3) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.3) 27 | activesupport (= 4.2.3) 28 | builder (~> 3.1) 29 | activerecord (4.2.3) 30 | activemodel (= 4.2.3) 31 | activesupport (= 4.2.3) 32 | arel (~> 6.0) 33 | activesupport (4.2.3) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | arel (6.0.2) 40 | bson (3.1.2) 41 | builder (3.2.2) 42 | carrierwave (0.10.0) 43 | activemodel (>= 3.2.0) 44 | activesupport (>= 3.2.0) 45 | json (>= 1.7) 46 | mime-types (>= 1.16) 47 | carrierwave-mongoid (0.7.1) 48 | carrierwave (>= 0.8.0, < 0.11.0) 49 | mongoid (>= 3.0, < 5.0) 50 | mongoid-grid_fs (>= 1.3, < 3.0) 51 | coderay (1.1.0) 52 | decent_exposure (2.3.2) 53 | diff-lcs (1.2.5) 54 | erubis (2.7.0) 55 | fabrication (2.13.2) 56 | ffi (1.9.10) 57 | formatador (0.2.5) 58 | globalid (0.3.5) 59 | activesupport (>= 4.1.0) 60 | guard (2.12.8) 61 | formatador (>= 0.2.4) 62 | listen (>= 2.7, <= 4.0) 63 | lumberjack (~> 1.0) 64 | nenv (~> 0.1) 65 | notiffany (~> 0.0) 66 | pry (>= 0.9.12) 67 | shellany (~> 0.0) 68 | thor (>= 0.18.1) 69 | guard-compat (1.2.1) 70 | guard-rspec (4.6.2) 71 | guard (~> 2.1) 72 | guard-compat (~> 1.1) 73 | rspec (>= 2.99.0, < 4.0) 74 | i18n (0.7.0) 75 | json (1.8.3) 76 | listen (3.0.2) 77 | rb-fsevent (>= 0.9.3) 78 | rb-inotify (>= 0.9) 79 | loofah (2.0.2) 80 | nokogiri (>= 1.5.9) 81 | lumberjack (1.0.9) 82 | mail (2.6.3) 83 | mime-types (>= 1.16, < 3) 84 | method_source (0.8.2) 85 | mime-types (2.6.1) 86 | mimetype-fu (0.1.2) 87 | mini_portile (0.6.2) 88 | minitest (5.7.0) 89 | mongo (2.1.0.beta) 90 | bson (~> 3.0) 91 | mongoid (5.0.0.beta) 92 | activemodel (~> 4.0) 93 | mongo (= 2.1.0.beta) 94 | origin (~> 2.1) 95 | tzinfo (>= 0.3.37) 96 | mongoid-grid_fs (2.1.0) 97 | mime-types (>= 1.0, < 3.0) 98 | mongoid (>= 3.0, < 5.0) 99 | nenv (0.2.0) 100 | nokogiri (1.6.6.2) 101 | mini_portile (~> 0.6.0) 102 | notiffany (0.0.6) 103 | nenv (~> 0.1) 104 | shellany (~> 0.0) 105 | origin (2.1.1) 106 | pry (0.10.1) 107 | coderay (~> 1.1.0) 108 | method_source (~> 0.8.1) 109 | slop (~> 3.4) 110 | rack (1.6.4) 111 | rack-test (0.6.3) 112 | rack (>= 1.0) 113 | rails (4.2.3) 114 | actionmailer (= 4.2.3) 115 | actionpack (= 4.2.3) 116 | actionview (= 4.2.3) 117 | activejob (= 4.2.3) 118 | activemodel (= 4.2.3) 119 | activerecord (= 4.2.3) 120 | activesupport (= 4.2.3) 121 | bundler (>= 1.3.0, < 2.0) 122 | railties (= 4.2.3) 123 | sprockets-rails 124 | rails-deprecated_sanitizer (1.0.3) 125 | activesupport (>= 4.2.0.alpha) 126 | rails-dom-testing (1.0.6) 127 | activesupport (>= 4.2.0.beta, < 5.0) 128 | nokogiri (~> 1.6.0) 129 | rails-deprecated_sanitizer (>= 1.0.1) 130 | rails-html-sanitizer (1.0.2) 131 | loofah (~> 2.0) 132 | railties (4.2.3) 133 | actionpack (= 4.2.3) 134 | activesupport (= 4.2.3) 135 | rake (>= 0.8.7) 136 | thor (>= 0.18.1, < 2.0) 137 | rake (10.4.2) 138 | rb-fsevent (0.9.5) 139 | rb-inotify (0.9.5) 140 | ffi (>= 0.5.0) 141 | responders (2.1.0) 142 | railties (>= 4.2.0, < 5) 143 | rspec (3.3.0) 144 | rspec-core (~> 3.3.0) 145 | rspec-expectations (~> 3.3.0) 146 | rspec-mocks (~> 3.3.0) 147 | rspec-core (3.3.2) 148 | rspec-support (~> 3.3.0) 149 | rspec-expectations (3.3.1) 150 | diff-lcs (>= 1.2.0, < 2.0) 151 | rspec-support (~> 3.3.0) 152 | rspec-mocks (3.3.2) 153 | diff-lcs (>= 1.2.0, < 2.0) 154 | rspec-support (~> 3.3.0) 155 | rspec-rails (3.3.3) 156 | actionpack (>= 3.0, < 4.3) 157 | activesupport (>= 3.0, < 4.3) 158 | railties (>= 3.0, < 4.3) 159 | rspec-core (~> 3.3.0) 160 | rspec-expectations (~> 3.3.0) 161 | rspec-mocks (~> 3.3.0) 162 | rspec-support (~> 3.3.0) 163 | rspec-support (3.3.0) 164 | shellany (0.0.1) 165 | slop (3.6.0) 166 | sprockets (3.2.0) 167 | rack (~> 1.0) 168 | sprockets-rails (2.3.2) 169 | actionpack (>= 3.0) 170 | activesupport (>= 3.0) 171 | sprockets (>= 2.8, < 4.0) 172 | thor (0.19.1) 173 | thread_safe (0.3.5) 174 | tzinfo (1.2.2) 175 | thread_safe (~> 0.1) 176 | 177 | PLATFORMS 178 | ruby 179 | 180 | DEPENDENCIES 181 | carrierwave 182 | carrierwave-mongoid 183 | decent_exposure 184 | fabrication 185 | guard-rspec 186 | jruby-openssl 187 | mimetype-fu 188 | mongoid (~> 5.0.0.beta) 189 | rails (~> 4.2) 190 | responders 191 | rspec-rails 192 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # vim:set filetype=ruby: 2 | guard( 3 | "rspec", 4 | all_after_pass: false, 5 | cli: "--fail-fast --tty --format documentation --colour") do 6 | 7 | watch(%r{^spec/.+_spec\.rb$}) 8 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 9 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 10 | watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } 11 | end 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Echo [![Build Status](https://secure.travis-ci.org/mongoid/echo.png?branch=master&.png)](http://travis-ci.org/mongoid/echo) 2 | ==== 3 | This is the sample Rails application for Mongoid. 4 | 5 | Compatibility 6 | ------------- 7 | 8 | Mongoid is tested against MRI 1.9.2, 1.9.3, 2.0.0, and JRuby (1.9). 9 | 10 | Development Environment Setup 11 | ----------------------------- 12 | 13 | * `$ brew install mongodb` 14 | * `$ gem install bundler` 15 | * `$ bundle install` 16 | 17 | Running Specs 18 | ------------- 19 | 20 | * `$ rake` 21 | 22 | Running Specs with Guard and Spork 23 | ----------------------------------- 24 | 25 | * `$ spork` 26 | * `$ bundle exec guard` 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | task(:test) {} 7 | 8 | Echo::Application.load_tasks 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require vendor/underscore 3 | //= require vendor/backbone 4 | //= require_tree . 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/vendor/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.5.3 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | (function(){ 7 | 8 | // Initial Setup 9 | // ------------- 10 | 11 | // Save a reference to the global object. 12 | var root = this; 13 | 14 | // Save the previous value of the `Backbone` variable. 15 | var previousBackbone = root.Backbone; 16 | 17 | // The top-level namespace. All public Backbone classes and modules will 18 | // be attached to this. Exported for both CommonJS and the browser. 19 | var Backbone; 20 | if (typeof exports !== 'undefined') { 21 | Backbone = exports; 22 | } else { 23 | Backbone = root.Backbone = {}; 24 | } 25 | 26 | // Current version of the library. Keep in sync with `package.json`. 27 | Backbone.VERSION = '0.5.3'; 28 | 29 | // Require Underscore, if we're on the server, and it's not already present. 30 | var _ = root._; 31 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; 32 | 33 | // For Backbone's purposes, jQuery or Zepto owns the `$` variable. 34 | var $ = root.jQuery || root.Zepto; 35 | 36 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 37 | // to its previous owner. Returns a reference to this Backbone object. 38 | Backbone.noConflict = function() { 39 | root.Backbone = previousBackbone; 40 | return this; 41 | }; 42 | 43 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will 44 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 45 | // `X-Http-Method-Override` header. 46 | Backbone.emulateHTTP = false; 47 | 48 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 49 | // `application/json` requests ... will encode the body as 50 | // `application/x-www-form-urlencoded` instead and will send the model in a 51 | // form param named `model`. 52 | Backbone.emulateJSON = false; 53 | 54 | // Backbone.Events 55 | // ----------------- 56 | 57 | // A module that can be mixed in to *any object* in order to provide it with 58 | // custom events. You may `bind` or `unbind` a callback function to an event; 59 | // `trigger`-ing an event fires all callbacks in succession. 60 | // 61 | // var object = {}; 62 | // _.extend(object, Backbone.Events); 63 | // object.bind('expand', function(){ alert('expanded'); }); 64 | // object.trigger('expand'); 65 | // 66 | Backbone.Events = { 67 | 68 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 69 | // Passing `"all"` will bind the callback to all events fired. 70 | bind : function(ev, callback, context) { 71 | var calls = this._callbacks || (this._callbacks = {}); 72 | var list = calls[ev] || (calls[ev] = []); 73 | list.push([callback, context]); 74 | return this; 75 | }, 76 | 77 | // Remove one or many callbacks. If `callback` is null, removes all 78 | // callbacks for the event. If `ev` is null, removes all bound callbacks 79 | // for all events. 80 | unbind : function(ev, callback) { 81 | var calls; 82 | if (!ev) { 83 | this._callbacks = {}; 84 | } else if (calls = this._callbacks) { 85 | if (!callback) { 86 | calls[ev] = []; 87 | } else { 88 | var list = calls[ev]; 89 | if (!list) return this; 90 | for (var i = 0, l = list.length; i < l; i++) { 91 | if (list[i] && callback === list[i][0]) { 92 | list[i] = null; 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | return this; 99 | }, 100 | 101 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 102 | // same arguments as `trigger` is, apart from the event name. 103 | // Listening for `"all"` passes the true event name as the first argument. 104 | trigger : function(eventName) { 105 | var list, calls, ev, callback, args; 106 | var both = 2; 107 | if (!(calls = this._callbacks)) return this; 108 | while (both--) { 109 | ev = both ? eventName : 'all'; 110 | if (list = calls[ev]) { 111 | for (var i = 0, l = list.length; i < l; i++) { 112 | if (!(callback = list[i])) { 113 | list.splice(i, 1); i--; l--; 114 | } else { 115 | args = both ? Array.prototype.slice.call(arguments, 1) : arguments; 116 | callback[0].apply(callback[1] || this, args); 117 | } 118 | } 119 | } 120 | } 121 | return this; 122 | } 123 | 124 | }; 125 | 126 | // Backbone.Model 127 | // -------------- 128 | 129 | // Create a new model, with defined attributes. A client id (`cid`) 130 | // is automatically generated and assigned for you. 131 | Backbone.Model = function(attributes, options) { 132 | var defaults; 133 | attributes || (attributes = {}); 134 | if (defaults = this.defaults) { 135 | if (_.isFunction(defaults)) defaults = defaults.call(this); 136 | attributes = _.extend({}, defaults, attributes); 137 | } 138 | this.attributes = {}; 139 | this._escapedAttributes = {}; 140 | this.cid = _.uniqueId('c'); 141 | this.set(attributes, {silent : true}); 142 | this._changed = false; 143 | this._previousAttributes = _.clone(this.attributes); 144 | if (options && options.collection) this.collection = options.collection; 145 | this.initialize(attributes, options); 146 | }; 147 | 148 | // Attach all inheritable methods to the Model prototype. 149 | _.extend(Backbone.Model.prototype, Backbone.Events, { 150 | 151 | // A snapshot of the model's previous attributes, taken immediately 152 | // after the last `"change"` event was fired. 153 | _previousAttributes : null, 154 | 155 | // Has the item been changed since the last `"change"` event? 156 | _changed : false, 157 | 158 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 159 | // CouchDB users may want to set this to `"_id"`. 160 | idAttribute : 'id', 161 | 162 | // Initialize is an empty function by default. Override it with your own 163 | // initialization logic. 164 | initialize : function(){}, 165 | 166 | // Return a copy of the model's `attributes` object. 167 | toJSON : function() { 168 | return _.clone(this.attributes); 169 | }, 170 | 171 | // Get the value of an attribute. 172 | get : function(attr) { 173 | return this.attributes[attr]; 174 | }, 175 | 176 | // Get the HTML-escaped value of an attribute. 177 | escape : function(attr) { 178 | var html; 179 | if (html = this._escapedAttributes[attr]) return html; 180 | var val = this.attributes[attr]; 181 | return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); 182 | }, 183 | 184 | // Returns `true` if the attribute contains a value that is not null 185 | // or undefined. 186 | has : function(attr) { 187 | return this.attributes[attr] != null; 188 | }, 189 | 190 | // Set a hash of model attributes on the object, firing `"change"` unless you 191 | // choose to silence it. 192 | set : function(attrs, options) { 193 | 194 | // Extract attributes and options. 195 | options || (options = {}); 196 | if (!attrs) return this; 197 | if (attrs.attributes) attrs = attrs.attributes; 198 | var now = this.attributes, escaped = this._escapedAttributes; 199 | 200 | // Run validation. 201 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 202 | 203 | // Check for changes of `id`. 204 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 205 | 206 | // We're about to start triggering change events. 207 | var alreadyChanging = this._changing; 208 | this._changing = true; 209 | 210 | // Update attributes. 211 | for (var attr in attrs) { 212 | var val = attrs[attr]; 213 | if (!_.isEqual(now[attr], val)) { 214 | now[attr] = val; 215 | delete escaped[attr]; 216 | this._changed = true; 217 | if (!options.silent) this.trigger('change:' + attr, this, val, options); 218 | } 219 | } 220 | 221 | // Fire the `"change"` event, if the model has been changed. 222 | if (!alreadyChanging && !options.silent && this._changed) this.change(options); 223 | this._changing = false; 224 | return this; 225 | }, 226 | 227 | // Remove an attribute from the model, firing `"change"` unless you choose 228 | // to silence it. `unset` is a noop if the attribute doesn't exist. 229 | unset : function(attr, options) { 230 | if (!(attr in this.attributes)) return this; 231 | options || (options = {}); 232 | var value = this.attributes[attr]; 233 | 234 | // Run validation. 235 | var validObj = {}; 236 | validObj[attr] = void 0; 237 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 238 | 239 | // Remove the attribute. 240 | delete this.attributes[attr]; 241 | delete this._escapedAttributes[attr]; 242 | if (attr == this.idAttribute) delete this.id; 243 | this._changed = true; 244 | if (!options.silent) { 245 | this.trigger('change:' + attr, this, void 0, options); 246 | this.change(options); 247 | } 248 | return this; 249 | }, 250 | 251 | // Clear all attributes on the model, firing `"change"` unless you choose 252 | // to silence it. 253 | clear : function(options) { 254 | options || (options = {}); 255 | var attr; 256 | var old = this.attributes; 257 | 258 | // Run validation. 259 | var validObj = {}; 260 | for (attr in old) validObj[attr] = void 0; 261 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 262 | 263 | this.attributes = {}; 264 | this._escapedAttributes = {}; 265 | this._changed = true; 266 | if (!options.silent) { 267 | for (attr in old) { 268 | this.trigger('change:' + attr, this, void 0, options); 269 | } 270 | this.change(options); 271 | } 272 | return this; 273 | }, 274 | 275 | // Fetch the model from the server. If the server's representation of the 276 | // model differs from its current attributes, they will be overriden, 277 | // triggering a `"change"` event. 278 | fetch : function(options) { 279 | options || (options = {}); 280 | var model = this; 281 | var success = options.success; 282 | options.success = function(resp, status, xhr) { 283 | if (!model.set(model.parse(resp, xhr), options)) return false; 284 | if (success) success(model, resp); 285 | }; 286 | options.error = wrapError(options.error, model, options); 287 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 288 | }, 289 | 290 | // Set a hash of model attributes, and sync the model to the server. 291 | // If the server returns an attributes hash that differs, the model's 292 | // state will be `set` again. 293 | save : function(attrs, options) { 294 | options || (options = {}); 295 | if (attrs && !this.set(attrs, options)) return false; 296 | var model = this; 297 | var success = options.success; 298 | options.success = function(resp, status, xhr) { 299 | if (!model.set(model.parse(resp, xhr), options)) return false; 300 | if (success) success(model, resp, xhr); 301 | }; 302 | options.error = wrapError(options.error, model, options); 303 | var method = this.isNew() ? 'create' : 'update'; 304 | return (this.sync || Backbone.sync).call(this, method, this, options); 305 | }, 306 | 307 | // Destroy this model on the server if it was already persisted. Upon success, the model is removed 308 | // from its collection, if it has one. 309 | destroy : function(options) { 310 | options || (options = {}); 311 | if (this.isNew()) return this.trigger('destroy', this, this.collection, options); 312 | var model = this; 313 | var success = options.success; 314 | options.success = function(resp) { 315 | model.trigger('destroy', model, model.collection, options); 316 | if (success) success(model, resp); 317 | }; 318 | options.error = wrapError(options.error, model, options); 319 | return (this.sync || Backbone.sync).call(this, 'delete', this, options); 320 | }, 321 | 322 | // Default URL for the model's representation on the server -- if you're 323 | // using Backbone's restful methods, override this to change the endpoint 324 | // that will be called. 325 | url : function() { 326 | var base = getUrl(this.collection) || this.urlRoot || urlError(); 327 | if (this.isNew()) return base; 328 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); 329 | }, 330 | 331 | // **parse** converts a response into the hash of attributes to be `set` on 332 | // the model. The default implementation is just to pass the response along. 333 | parse : function(resp, xhr) { 334 | return resp; 335 | }, 336 | 337 | // Create a new model with identical attributes to this one. 338 | clone : function() { 339 | return new this.constructor(this); 340 | }, 341 | 342 | // A model is new if it has never been saved to the server, and lacks an id. 343 | isNew : function() { 344 | return this.id == null; 345 | }, 346 | 347 | // Call this method to manually fire a `change` event for this model. 348 | // Calling this will cause all objects observing the model to update. 349 | change : function(options) { 350 | this.trigger('change', this, options); 351 | this._previousAttributes = _.clone(this.attributes); 352 | this._changed = false; 353 | }, 354 | 355 | // Determine if the model has changed since the last `"change"` event. 356 | // If you specify an attribute name, determine if that attribute has changed. 357 | hasChanged : function(attr) { 358 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 359 | return this._changed; 360 | }, 361 | 362 | // Return an object containing all the attributes that have changed, or false 363 | // if there are no changed attributes. Useful for determining what parts of a 364 | // view need to be updated and/or what attributes need to be persisted to 365 | // the server. 366 | changedAttributes : function(now) { 367 | now || (now = this.attributes); 368 | var old = this._previousAttributes; 369 | var changed = false; 370 | for (var attr in now) { 371 | if (!_.isEqual(old[attr], now[attr])) { 372 | changed = changed || {}; 373 | changed[attr] = now[attr]; 374 | } 375 | } 376 | return changed; 377 | }, 378 | 379 | // Get the previous value of an attribute, recorded at the time the last 380 | // `"change"` event was fired. 381 | previous : function(attr) { 382 | if (!attr || !this._previousAttributes) return null; 383 | return this._previousAttributes[attr]; 384 | }, 385 | 386 | // Get all of the attributes of the model at the time of the previous 387 | // `"change"` event. 388 | previousAttributes : function() { 389 | return _.clone(this._previousAttributes); 390 | }, 391 | 392 | // Run validation against a set of incoming attributes, returning `true` 393 | // if all is well. If a specific `error` callback has been passed, 394 | // call that instead of firing the general `"error"` event. 395 | _performValidation : function(attrs, options) { 396 | var error = this.validate(attrs); 397 | if (error) { 398 | if (options.error) { 399 | options.error(this, error, options); 400 | } else { 401 | this.trigger('error', this, error, options); 402 | } 403 | return false; 404 | } 405 | return true; 406 | } 407 | 408 | }); 409 | 410 | // Backbone.Collection 411 | // ------------------- 412 | 413 | // Provides a standard collection class for our sets of models, ordered 414 | // or unordered. If a `comparator` is specified, the Collection will maintain 415 | // its models in sort order, as they're added and removed. 416 | Backbone.Collection = function(models, options) { 417 | options || (options = {}); 418 | if (options.comparator) this.comparator = options.comparator; 419 | _.bindAll(this, '_onModelEvent', '_removeReference'); 420 | this._reset(); 421 | if (models) this.reset(models, {silent: true}); 422 | this.initialize.apply(this, arguments); 423 | }; 424 | 425 | // Define the Collection's inheritable methods. 426 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 427 | 428 | // The default model for a collection is just a **Backbone.Model**. 429 | // This should be overridden in most cases. 430 | model : Backbone.Model, 431 | 432 | // Initialize is an empty function by default. Override it with your own 433 | // initialization logic. 434 | initialize : function(){}, 435 | 436 | // The JSON representation of a Collection is an array of the 437 | // models' attributes. 438 | toJSON : function() { 439 | return this.map(function(model){ return model.toJSON(); }); 440 | }, 441 | 442 | // Add a model, or list of models to the set. Pass **silent** to avoid 443 | // firing the `added` event for every new model. 444 | add : function(models, options) { 445 | if (_.isArray(models)) { 446 | for (var i = 0, l = models.length; i < l; i++) { 447 | this._add(models[i], options); 448 | } 449 | } else { 450 | this._add(models, options); 451 | } 452 | return this; 453 | }, 454 | 455 | // Remove a model, or a list of models from the set. Pass silent to avoid 456 | // firing the `removed` event for every model removed. 457 | remove : function(models, options) { 458 | if (_.isArray(models)) { 459 | for (var i = 0, l = models.length; i < l; i++) { 460 | this._remove(models[i], options); 461 | } 462 | } else { 463 | this._remove(models, options); 464 | } 465 | return this; 466 | }, 467 | 468 | // Get a model from the set by id. 469 | get : function(id) { 470 | if (id == null) return null; 471 | return this._byId[id.id != null ? id.id : id]; 472 | }, 473 | 474 | // Get a model from the set by client id. 475 | getByCid : function(cid) { 476 | return cid && this._byCid[cid.cid || cid]; 477 | }, 478 | 479 | // Get the model at the given index. 480 | at: function(index) { 481 | return this.models[index]; 482 | }, 483 | 484 | // Force the collection to re-sort itself. You don't need to call this under normal 485 | // circumstances, as the set will maintain sort order as each item is added. 486 | sort : function(options) { 487 | options || (options = {}); 488 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 489 | this.models = this.sortBy(this.comparator); 490 | if (!options.silent) this.trigger('reset', this, options); 491 | return this; 492 | }, 493 | 494 | // Pluck an attribute from each model in the collection. 495 | pluck : function(attr) { 496 | return _.map(this.models, function(model){ return model.get(attr); }); 497 | }, 498 | 499 | // When you have more items than you want to add or remove individually, 500 | // you can reset the entire set with a new list of models, without firing 501 | // any `added` or `removed` events. Fires `reset` when finished. 502 | reset : function(models, options) { 503 | models || (models = []); 504 | options || (options = {}); 505 | this.each(this._removeReference); 506 | this._reset(); 507 | this.add(models, {silent: true}); 508 | if (!options.silent) this.trigger('reset', this, options); 509 | return this; 510 | }, 511 | 512 | // Fetch the default set of models for this collection, resetting the 513 | // collection when they arrive. If `add: true` is passed, appends the 514 | // models to the collection instead of resetting. 515 | fetch : function(options) { 516 | options || (options = {}); 517 | var collection = this; 518 | var success = options.success; 519 | options.success = function(resp, status, xhr) { 520 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); 521 | if (success) success(collection, resp); 522 | }; 523 | options.error = wrapError(options.error, collection, options); 524 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 525 | }, 526 | 527 | // Create a new instance of a model in this collection. After the model 528 | // has been created on the server, it will be added to the collection. 529 | // Returns the model, or 'false' if validation on a new model fails. 530 | create : function(model, options) { 531 | var coll = this; 532 | options || (options = {}); 533 | model = this._prepareModel(model, options); 534 | if (!model) return false; 535 | var success = options.success; 536 | options.success = function(nextModel, resp, xhr) { 537 | coll.add(nextModel, options); 538 | if (success) success(nextModel, resp, xhr); 539 | }; 540 | model.save(null, options); 541 | return model; 542 | }, 543 | 544 | // **parse** converts a response into a list of models to be added to the 545 | // collection. The default implementation is just to pass it through. 546 | parse : function(resp, xhr) { 547 | return resp; 548 | }, 549 | 550 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 551 | // underscore methods are proxied because it relies on the underscore 552 | // constructor. 553 | chain: function () { 554 | return _(this.models).chain(); 555 | }, 556 | 557 | // Reset all internal state. Called when the collection is reset. 558 | _reset : function(options) { 559 | this.length = 0; 560 | this.models = []; 561 | this._byId = {}; 562 | this._byCid = {}; 563 | }, 564 | 565 | // Prepare a model to be added to this collection 566 | _prepareModel: function(model, options) { 567 | if (!(model instanceof Backbone.Model)) { 568 | var attrs = model; 569 | model = new this.model(attrs, {collection: this}); 570 | if (model.validate && !model._performValidation(attrs, options)) model = false; 571 | } else if (!model.collection) { 572 | model.collection = this; 573 | } 574 | return model; 575 | }, 576 | 577 | // Internal implementation of adding a single model to the set, updating 578 | // hash indexes for `id` and `cid` lookups. 579 | // Returns the model, or 'false' if validation on a new model fails. 580 | _add : function(model, options) { 581 | options || (options = {}); 582 | model = this._prepareModel(model, options); 583 | if (!model) return false; 584 | var already = this.getByCid(model); 585 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 586 | this._byId[model.id] = model; 587 | this._byCid[model.cid] = model; 588 | var index = options.at != null ? options.at : 589 | this.comparator ? this.sortedIndex(model, this.comparator) : 590 | this.length; 591 | this.models.splice(index, 0, model); 592 | model.bind('all', this._onModelEvent); 593 | this.length++; 594 | if (!options.silent) model.trigger('add', model, this, options); 595 | return model; 596 | }, 597 | 598 | // Internal implementation of removing a single model from the set, updating 599 | // hash indexes for `id` and `cid` lookups. 600 | _remove : function(model, options) { 601 | options || (options = {}); 602 | model = this.getByCid(model) || this.get(model); 603 | if (!model) return null; 604 | delete this._byId[model.id]; 605 | delete this._byCid[model.cid]; 606 | this.models.splice(this.indexOf(model), 1); 607 | this.length--; 608 | if (!options.silent) model.trigger('remove', model, this, options); 609 | this._removeReference(model); 610 | return model; 611 | }, 612 | 613 | // Internal method to remove a model's ties to a collection. 614 | _removeReference : function(model) { 615 | if (this == model.collection) { 616 | delete model.collection; 617 | } 618 | model.unbind('all', this._onModelEvent); 619 | }, 620 | 621 | // Internal method called every time a model in the set fires an event. 622 | // Sets need to update their indexes when models change ids. All other 623 | // events simply proxy through. "add" and "remove" events that originate 624 | // in other collections are ignored. 625 | _onModelEvent : function(ev, model, collection, options) { 626 | if ((ev == 'add' || ev == 'remove') && collection != this) return; 627 | if (ev == 'destroy') { 628 | this._remove(model, options); 629 | } 630 | if (model && ev === 'change:' + model.idAttribute) { 631 | delete this._byId[model.previous(model.idAttribute)]; 632 | this._byId[model.id] = model; 633 | } 634 | this.trigger.apply(this, arguments); 635 | } 636 | 637 | }); 638 | 639 | // Underscore methods that we want to implement on the Collection. 640 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 641 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 642 | 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 643 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; 644 | 645 | // Mix in each Underscore method as a proxy to `Collection#models`. 646 | _.each(methods, function(method) { 647 | Backbone.Collection.prototype[method] = function() { 648 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 649 | }; 650 | }); 651 | 652 | // Backbone.Router 653 | // ------------------- 654 | 655 | // Routers map faux-URLs to actions, and fire events when routes are 656 | // matched. Creating a new one sets its `routes` hash, if not set statically. 657 | Backbone.Router = function(options) { 658 | options || (options = {}); 659 | if (options.routes) this.routes = options.routes; 660 | this._bindRoutes(); 661 | this.initialize.apply(this, arguments); 662 | }; 663 | 664 | // Cached regular expressions for matching named param parts and splatted 665 | // parts of route strings. 666 | var namedParam = /:([\w\d]+)/g; 667 | var splatParam = /\*([\w\d]+)/g; 668 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 669 | 670 | // Set up all inheritable **Backbone.Router** properties and methods. 671 | _.extend(Backbone.Router.prototype, Backbone.Events, { 672 | 673 | // Initialize is an empty function by default. Override it with your own 674 | // initialization logic. 675 | initialize : function(){}, 676 | 677 | // Manually bind a single named route to a callback. For example: 678 | // 679 | // this.route('search/:query/p:num', 'search', function(query, num) { 680 | // ... 681 | // }); 682 | // 683 | route : function(route, name, callback) { 684 | Backbone.history || (Backbone.history = new Backbone.History); 685 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 686 | Backbone.history.route(route, _.bind(function(fragment) { 687 | var args = this._extractParameters(route, fragment); 688 | callback.apply(this, args); 689 | this.trigger.apply(this, ['route:' + name].concat(args)); 690 | }, this)); 691 | }, 692 | 693 | // Simple proxy to `Backbone.history` to save a fragment into the history. 694 | navigate : function(fragment, triggerRoute) { 695 | Backbone.history.navigate(fragment, triggerRoute); 696 | }, 697 | 698 | // Bind all defined routes to `Backbone.history`. We have to reverse the 699 | // order of the routes here to support behavior where the most general 700 | // routes can be defined at the bottom of the route map. 701 | _bindRoutes : function() { 702 | if (!this.routes) return; 703 | var routes = []; 704 | for (var route in this.routes) { 705 | routes.unshift([route, this.routes[route]]); 706 | } 707 | for (var i = 0, l = routes.length; i < l; i++) { 708 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]); 709 | } 710 | }, 711 | 712 | // Convert a route string into a regular expression, suitable for matching 713 | // against the current location hash. 714 | _routeToRegExp : function(route) { 715 | route = route.replace(escapeRegExp, "\\$&") 716 | .replace(namedParam, "([^\/]*)") 717 | .replace(splatParam, "(.*?)"); 718 | return new RegExp('^' + route + '$'); 719 | }, 720 | 721 | // Given a route, and a URL fragment that it matches, return the array of 722 | // extracted parameters. 723 | _extractParameters : function(route, fragment) { 724 | return route.exec(fragment).slice(1); 725 | } 726 | 727 | }); 728 | 729 | // Backbone.History 730 | // ---------------- 731 | 732 | // Handles cross-browser history management, based on URL fragments. If the 733 | // browser does not support `onhashchange`, falls back to polling. 734 | Backbone.History = function() { 735 | this.handlers = []; 736 | _.bindAll(this, 'checkUrl'); 737 | }; 738 | 739 | // Cached regex for cleaning hashes. 740 | var hashStrip = /^#*/; 741 | 742 | // Cached regex for detecting MSIE. 743 | var isExplorer = /msie [\w.]+/; 744 | 745 | // Has the history handling already been started? 746 | var historyStarted = false; 747 | 748 | // Set up all inheritable **Backbone.History** properties and methods. 749 | _.extend(Backbone.History.prototype, { 750 | 751 | // The default interval to poll for hash changes, if necessary, is 752 | // twenty times a second. 753 | interval: 50, 754 | 755 | // Get the cross-browser normalized URL fragment, either from the URL, 756 | // the hash, or the override. 757 | getFragment : function(fragment, forcePushState) { 758 | if (fragment == null) { 759 | if (this._hasPushState || forcePushState) { 760 | fragment = window.location.pathname; 761 | var search = window.location.search; 762 | if (search) fragment += search; 763 | if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); 764 | } else { 765 | fragment = window.location.hash; 766 | } 767 | } 768 | return decodeURIComponent(fragment.replace(hashStrip, '')); 769 | }, 770 | 771 | // Start the hash change handling, returning `true` if the current URL matches 772 | // an existing route, and `false` otherwise. 773 | start : function(options) { 774 | 775 | // Figure out the initial configuration. Do we need an iframe? 776 | // Is pushState desired ... is it available? 777 | if (historyStarted) throw new Error("Backbone.history has already been started"); 778 | this.options = _.extend({}, {root: '/'}, this.options, options); 779 | this._wantsPushState = !!this.options.pushState; 780 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); 781 | var fragment = this.getFragment(); 782 | var docMode = document.documentMode; 783 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 784 | if (oldIE) { 785 | this.iframe = $('