From dcba2b88024522f3a5970719405ae1136333e3da Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Wed, 13 Mar 2013 17:01:16 -0300 Subject: [PATCH 001/102] updated submodule to catarse new repo and changed Factory to FactoryGirl --- .gitmodules | 2 +- Gemfile | 122 +++--- Gemfile.lock | 404 ++++++++---------- .../payment/moip_controller_spec.rb | 2 +- spec/spec_helper.rb | 2 +- test/dummy | 2 +- 6 files changed, 257 insertions(+), 277 deletions(-) diff --git a/.gitmodules b/.gitmodules index fad9d6f..f97bc25 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "test/dummy"] path = test/dummy - url = git://github.com/danielweinmann/catarse.git + url = git://github.com/catarse/catarse.git diff --git a/Gemfile b/Gemfile index d4cc3ca..281811d 100644 --- a/Gemfile +++ b/Gemfile @@ -4,30 +4,39 @@ source "http://rubygems.org" # Bundler will treat runtime dependencies like base dependencies, and # development dependencies will be added by default to the :development group. gemspec -source :gemcutter -source 'http://gems.github.com' -gem 'rails', '3.2.11' +ruby '1.9.3' -gem 'sidekiq', '= 2.4.0' -gem 'sinatra', require: false -gem 'foreman' +gem 'rails', '3.2.12' +gem 'sidekiq', '~> 2.7.5' +gem 'sinatra', require: false # required by sidekiq web interface mounted on /sidekiq + +# Turns every field on a editable one gem 'best_in_place' +# State machine for attributes on models gem 'state_machine', require: 'state_machine/core' +# paranoid stuff +gem 'paper_trail', '~> 2.7.1' + # Database and data related gem 'pg' gem 'pg_search' gem 'postgres-copy' gem 'schema_plus' +gem 'schema_associations' + +# Payment engine using Paypal +gem 'catarse_paypal_express', git: 'git://github.com/catarse/catarse_paypal_express.git', ref: 'bce4d8c' + +# Payment engine using Moip +#gem 'catarse_moip', git: 'git://github.com/catarse/catarse_moip.git', ref: '98f992428fcd5c4fc6edabd647ace9425b1e578a' -gem 'catarse_paypal_express', git: 'git://github.com/devton/catarse_paypal_express.git', ref: '4fd17e269395ee4b3a32528ace0bcb7eec57a953' -#gem 'catarse_paypal_express', path: '../catarse_paypal_express' -#gem 'catarse_moip', git: 'git://github.com/devton/catarse_moip.git' -#gem 'catarse_moip', path: '../catarse_moip' -gem 'moip', git: 'git://github.com/moiplabs/moip-ruby.git' +# TODO: Check the Catarse_Moip dependency +gem 'moip', '2.1.2' +# Decorators gem 'draper' # Frontend stuff @@ -37,14 +46,13 @@ gem 'jquery-rails' gem 'initjs' # Authentication and Authorization -gem 'omniauth', "~> 1.1.0" -gem 'omniauth-openid', '~> 1.0.1' -gem 'omniauth-twitter', '~> 0.0.12' -gem 'omniauth-facebook', '~> 1.2.0' -gem 'omniauth-github', '~> 1.0.1' -gem 'omniauth-linkedin', '~> 0.0.6' -gem 'omniauth-yahoo', '~> 0.0.4' -gem 'devise', '1.5.3' +gem 'omniauth' +gem 'omniauth-twitter' +gem 'omniauth-facebook', '1.4.0' +gem 'devise' + +# See https://github.com/ryanb/cancan/tree/2.0 for help about this +# In resume: this version of cancan allow checking for authorization on specific fields on the model gem 'cancan', git: 'git://github.com/ryanb/cancan.git', branch: '2.0', ref: 'f1cebde51a87be149b4970a3287826bb63c0ac0b' @@ -52,14 +60,12 @@ gem 'cancan', git: 'git://github.com/ryanb/cancan.git', branch: '2.0', ref: 'f1c gem "airbrake" # Email marketing -#gem 'mailchimp' -gem 'catarse_mailchimp', git: 'git://github.com/devton/catarse_mailchimp' +gem 'catarse_mailchimp', git: 'git://github.com/catarse/catarse_mailchimp' # HTML manipulation and formatting -gem 'formtastic', "~> 2.1.1" -gem "auto_html", '= 1.4.2' +gem 'formtastic', '~> 2.1.1' +gem "auto_html", '= 1.4.2' gem 'kaminari' -gem 'rails_autolink', '~> 1.0.7' # Uploads gem 'carrierwave', '~> 0.7.0' @@ -68,55 +74,61 @@ gem 'fog' # Other Tools gem 'feedzirra' -gem 'validation_reflection', git: 'git://github.com/ncri/validation_reflection.git' -gem 'inherited_resources', '1.3.1' +gem 'validation_reflection', git: 'git://github.com/ncri/validation_reflection.git' +gem 'inherited_resources', '1.3.1' gem 'has_scope' -gem 'spectator-validates_email', require: 'validates_email' -gem 'has_vimeo_video', '~> 0.0.5' -gem 'memoist', '~> 0.2.0' -gem 'wirble' -gem "on_the_spot" -gem 'weekdays' -gem 'brcep' -gem "RedCloth" -gem 'unicode' +gem 'spectator-validates_email', require: 'validates_email' +gem 'has_vimeo_video', '~> 0.0.5' gem 'enumerate_it' -gem 'httparty', '~> 0.6.1' -gem "rack-timeout" +gem 'httparty', '~> 0.8.1' # Translations gem 'http_accept_language' -gem 'routing-filter' #, :git => 'git://github.com/svenfuchs/routing-filter.git' - -# Administration -gem "meta_search", "1.1.3" +gem 'routing-filter' # Payment gem 'activemerchant', '1.17.0', require: 'active_merchant' -gem 'httpclient', '2.2.5' -gem 'selenium-webdriver' +gem 'httpclient', '2.2.5' # Server gem 'thin' -group :assets do - gem 'sass-rails', '~> 3.2.5' - gem 'coffee-rails', '~> 3.2.2' - gem "compass-rails", "~> 1.0.2" - gem 'uglifier', '>= 1.0.3' - gem 'compass-960-plugin', '~> 0.10.4' +group :development do + gem 'mailcatcher' + gem 'foreman' end group :test, :development do + gem 'rspec-rails' +end + +group :test do gem 'launchy' gem 'database_cleaner' - gem 'rspec-rails', "~> 2.10.0" - gem 'mocha', '0.10.4' + gem 'mocha', '~> 0.10.4' gem 'shoulda' - gem 'factory_girl_rails', '1.7.0' - gem 'capybara', ">= 1.0.1" + gem 'factory_girl_rails' + gem 'capybara', '~> 2.0.2' end -group :development do - gem 'mailcatcher' + +group :assets do + gem 'sass-rails', '~> 3.2.5' + gem 'coffee-rails', '~> 3.2.2' + gem "compass-rails", '~> 1.0.2' + gem 'uglifier', '~> 1.0.3' + gem 'compass-960-plugin', '~> 0.10.4' end + + + +# FIXME: Not-anymore-on-development +# Gems that are with 1 or more years on the vacuum +gem 'weekdays' +gem "rack-timeout" + +# TODO: Take a look on dependencies. Why not auto_html? +gem 'rails_autolink', '~> 1.0.7' + +# TODO: Take a look on dependencies +gem "RedCloth" diff --git a/Gemfile.lock b/Gemfile.lock index 97159ce..a08ca74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,5 +1,5 @@ GIT - remote: git://github.com/devton/catarse_mailchimp + remote: git://github.com/catarse/catarse_mailchimp revision: a5be917969a1df6010fbbdea25a5c1407c6f8659 specs: catarse_mailchimp (0.0.1) @@ -7,23 +7,14 @@ GIT supermodel GIT - remote: git://github.com/devton/catarse_paypal_express.git - revision: 4fd17e269395ee4b3a32528ace0bcb7eec57a953 - ref: 4fd17e269395ee4b3a32528ace0bcb7eec57a953 + remote: git://github.com/catarse/catarse_paypal_express.git + revision: bce4d8c0c76279382f10cad101f48de4e109e3a8 + ref: bce4d8c specs: catarse_paypal_express (0.1.0) activemerchant (~> 1.17.0) rails (~> 3.2.6) -GIT - remote: git://github.com/moiplabs/moip-ruby.git - revision: 2b61a77355cb8c5487ff68be0eef8873696a110e - specs: - moip (1.0.2) - activesupport (>= 2.3.2) - httparty (~> 0.6.1) - nokogiri (~> 1.4.3) - GIT remote: git://github.com/ncri/validation_reflection.git revision: 60320e6beb088808fd625a8d958dbd0d2661d494 @@ -47,20 +38,18 @@ PATH GEM remote: http://rubygems.org/ - remote: http://rubygems.org/ - remote: http://gems.github.com/ specs: RedCloth (4.2.9) - actionmailer (3.2.11) - actionpack (= 3.2.11) + actionmailer (3.2.12) + actionpack (= 3.2.12) mail (~> 2.4.4) - actionpack (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) + actionpack (3.2.12) + activemodel (= 3.2.12) + activesupport (= 3.2.12) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) - rack (~> 1.4.0) + rack (~> 1.4.5) rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) @@ -69,24 +58,25 @@ GEM braintree (>= 2.0.0) builder (>= 2.0.0) json (>= 1.5.1) - activemodel (3.2.11) - activesupport (= 3.2.11) + activemodel (3.2.12) + activesupport (= 3.2.12) builder (~> 3.0.0) - activerecord (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) + activerecord (3.2.12) + activemodel (= 3.2.12) + activesupport (= 3.2.12) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) - activesupport (3.2.11) + activeresource (3.2.12) + activemodel (= 3.2.12) + activesupport (= 3.2.12) + activesupport (3.2.12) i18n (~> 0.6) multi_json (~> 1.0) - addressable (2.3.2) - airbrake (3.1.2) + addressable (2.3.3) + airbrake (3.1.8) activesupport builder + json arel (3.0.2) auto_html (1.4.2) RedCloth @@ -94,36 +84,37 @@ GEM rinku tag_helper bcrypt-ruby (3.0.1) - best_in_place (2.0.3) + best_in_place (2.1.0) jquery-rails rails (~> 3.1) - braintree (2.16.0) + bourne (1.1.2) + mocha (= 0.10.5) + braintree (2.22.0) builder (>= 2.0.0) - brcep (3.2.0) builder (3.0.4) - capybara (1.1.2) + capybara (2.0.2) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) selenium-webdriver (~> 2.0) - xpath (~> 0.1.4) + xpath (~> 1.0.0) carrierwave (0.7.1) activemodel (>= 3.2.0) activesupport (>= 3.2.0) - celluloid (0.12.3) + celluloid (0.12.4) facter (>= 1.6.12) timers (>= 1.0.0) - childprocess (0.3.5) - ffi (~> 1.0, >= 1.0.6) - chunky_png (1.2.6) + childprocess (0.3.9) + ffi (~> 1.0, >= 1.0.11) + chunky_png (1.2.7) coffee-rails (3.2.2) coffee-script (>= 2.2.0) railties (~> 3.2.0) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.3.3) + coffee-script-source (1.6.1) compass (0.12.2) chunky_png (~> 1.2) fssm (>= 0.2.7) @@ -132,33 +123,34 @@ GEM compass (>= 0.10.0) compass-rails (1.0.3) compass (>= 0.12.2, < 0.14) - connection_pool (0.9.2) - crack (0.1.8) + connection_pool (1.0.0) curb (0.7.18) daemons (1.1.9) - database_cleaner (0.8.0) - devise (1.5.3) + database_cleaner (0.9.1) + devise (2.2.3) bcrypt-ruby (~> 3.0) - orm_adapter (~> 0.0.3) - warden (~> 1.1) - diff-lcs (1.1.3) - draper (0.17.0) - actionpack (~> 3.2) - activesupport (~> 3.2) - enumerate_it (0.7.17) + orm_adapter (~> 0.1) + railties (~> 3.1) + warden (~> 1.2.1) + diff-lcs (1.2.1) + draper (1.1.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + request_store (~> 1.0.3) + enumerate_it (1.0.2) activesupport (>= 3.0.0) erubis (2.7.0) - eventmachine (0.12.10) + eventmachine (1.0.3) excon (0.6.6) execjs (1.4.0) multi_json (~> 1.0) - facter (1.6.16) - factory_girl (2.6.4) - activesupport (>= 2.3.9) - factory_girl_rails (1.7.0) - factory_girl (~> 2.6.0) + facter (1.6.17) + factory_girl (4.2.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.2.1) + factory_girl (~> 4.2.0) railties (>= 3.0.0) - faraday (0.8.4) + faraday (0.8.6) multipart-post (~> 1.1) feedzirra (0.0.31) activesupport (>= 3.0.8) @@ -170,7 +162,7 @@ GEM rake (>= 0.9.2) rdoc (~> 3.8) sax-machine (~> 0.0.20) - ffi (1.1.5) + ffi (1.4.0) fog (0.9.0) builder excon (~> 0.6.1) @@ -181,13 +173,14 @@ GEM net-ssh (>= 2.1.4) nokogiri (>= 1.4.4) ruby-hmac - foreman (0.60.2) + foreman (0.62.0) thor (>= 0.13.6) - formatador (0.2.3) + formatador (0.2.4) formtastic (2.1.1) actionpack (~> 3.0) - fssm (0.2.9) - haml (3.1.7) + fssm (0.2.10) + haml (4.0.0) + tilt has_scope (0.5.1) has_vimeo_video (0.0.5) supermodel @@ -195,31 +188,30 @@ GEM hashie (1.2.0) hike (1.2.1) http_accept_language (1.0.2) - httparty (0.6.1) - crack (= 0.1.8) - httpauth (0.1) + httparty (0.8.3) + multi_json (~> 1.0) + multi_xml + httpauth (0.2.0) httpclient (2.2.5) - i18n (0.6.1) + i18n (0.6.4) inherited_resources (1.3.1) has_scope (~> 0.5.0) responders (~> 0.6) - initjs (0.1.2) - rails (~> 3.1) + initjs (1.0.0) + jquery-rails + rails (>= 3.1) journey (1.0.4) - jquery-rails (2.1.1) - railties (>= 3.1.0, < 5.0) - thor (~> 0.14) - json (1.7.6) - json_pure (1.7.5) - jwt (0.1.5) - multi_json (>= 1.0) - kaminari (0.14.0) + jquery-rails (2.2.1) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) + json (1.7.7) + jwt (0.1.7) + multi_json (>= 1.5) + kaminari (0.14.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - launchy (2.1.2) + launchy (2.2.0) addressable (~> 2.3) - libwebsocket (0.1.5) - addressable libxml-ruby (2.3.3) loofah (1.0.0) nokogiri (>= 1.3.3) @@ -227,210 +219,195 @@ GEM i18n (>= 0.4.0) mime-types (~> 1.16) treetop (~> 1.4.8) - mailcatcher (0.5.8) + mailcatcher (0.5.11) activesupport (~> 3.0) - eventmachine (~> 0.12) - haml (~> 3.1) + eventmachine (~> 1.0.0) + haml (>= 3.1, < 5) mail (~> 2.3) sinatra (~> 1.2) - skinny (~> 0.2, >= 0.2.1) + skinny (~> 0.2.3) sqlite3 (~> 1.3) - thin (~> 1.2) - mailchimp (0.0.7.alpha) + thin (~> 1.5.0) + mailchimp (0.0.8) httparty - memoist (0.2.0) - meta_search (1.1.3) - actionpack (~> 3.1) - activerecord (~> 3.1) - activesupport (~> 3.1) - polyamorous (~> 0.5.0) metaclass (0.0.1) - mime-types (1.20.1) - mocha (0.10.4) + mime-types (1.21) + mocha (0.10.5) metaclass (~> 0.0.1) - multi_json (1.5.0) - multipart-post (1.1.5) - net-scp (1.0.4) - net-ssh (>= 1.99.1) - net-ssh (2.5.2) + moip (2.1.2) + activesupport (>= 2.3.2) + httparty (~> 0.8.1) + moip + nokogiri (~> 1.4.3) + multi_json (1.6.1) + multi_xml (0.5.3) + multipart-post (1.2.0) + net-scp (1.1.0) + net-ssh (>= 2.6.5) + net-ssh (2.6.6) nokogiri (1.4.7) - oauth (0.4.6) - oauth2 (0.8.0) + oauth (0.4.7) + oauth2 (0.8.1) faraday (~> 0.8) httpauth (~> 0.1) jwt (~> 0.1.4) multi_json (~> 1.0) rack (~> 1.2) - omniauth (1.1.1) + omniauth (1.1.3) hashie (~> 1.2) rack - omniauth-facebook (1.2.0) - omniauth-oauth2 (~> 1.0.0) - omniauth-github (1.0.1) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.0) - omniauth-linkedin (0.0.8) - omniauth-oauth (~> 1.0) + omniauth-facebook (1.4.0) + omniauth-oauth2 (~> 1.0.2) omniauth-oauth (1.0.1) oauth omniauth (~> 1.0) omniauth-oauth2 (1.0.3) oauth2 (~> 0.8.0) omniauth (~> 1.0) - omniauth-openid (1.0.1) - omniauth (~> 1.0) - rack-openid (~> 1.3.1) - omniauth-twitter (0.0.12) + omniauth-twitter (0.0.14) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - omniauth-yahoo (0.0.4) - omniauth-oauth (~> 1.0) - on_the_spot (1.0.1) - json_pure (>= 1.4.6) - orm_adapter (0.0.7) + orm_adapter (0.4.0) + paper_trail (2.7.1) + activerecord (~> 3.0) + railties (~> 3.0) pg (0.14.1) pg_search (0.5.7) activerecord (>= 3) activesupport (>= 3) - polyamorous (0.5.0) - activerecord (~> 3.0) polyglot (0.3.3) postgres-copy (0.5.7) activerecord (>= 3.0.0) pg rails (>= 3.0.0) responders - rack (1.4.4) + rack (1.4.5) rack-cache (1.2) rack (>= 0.4) - rack-openid (1.3.1) - rack (>= 1.1.0) - ruby-openid (>= 2.1.8) - rack-protection (1.2.0) + rack-protection (1.5.0) rack rack-ssl (1.3.3) rack rack-test (0.6.2) rack (>= 1.0) rack-timeout (0.0.3) - rails (3.2.11) - actionmailer (= 3.2.11) - actionpack (= 3.2.11) - activerecord (= 3.2.11) - activeresource (= 3.2.11) - activesupport (= 3.2.11) + rails (3.2.12) + actionmailer (= 3.2.12) + actionpack (= 3.2.12) + activerecord (= 3.2.12) + activeresource (= 3.2.12) + activesupport (= 3.2.12) bundler (~> 1.0) - railties (= 3.2.11) + railties (= 3.2.12) rails_autolink (1.0.9) rails (~> 3.1) - railties (3.2.11) - actionpack (= 3.2.11) - activesupport (= 3.2.11) + railties (3.2.12) + actionpack (= 3.2.12) + activesupport (= 3.2.12) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) rake (10.0.3) - rdoc (3.12) + rdoc (3.12.2) json (~> 1.4) - redcarpet (2.1.1) - redis (3.0.2) + redcarpet (2.2.2) + redis (3.0.3) redis-namespace (1.2.1) redis (~> 3.0.0) - responders (0.9.2) + request_store (1.0.5) + responders (0.9.3) railties (~> 3.1) - rinku (1.7.0) - rmagick (2.13.1) + rinku (1.7.2) + rmagick (2.13.2) routing-filter (0.3.1) actionpack - rspec (2.10.0) - rspec-core (~> 2.10.0) - rspec-expectations (~> 2.10.0) - rspec-mocks (~> 2.10.0) - rspec-core (2.10.1) - rspec-expectations (2.10.0) - diff-lcs (~> 1.1.3) - rspec-mocks (2.10.1) - rspec-rails (2.10.1) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.0) + rspec-rails (2.13.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec (~> 2.10.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) ruby-hmac (0.4.0) - ruby-openid (2.2.0) rubyzip (0.9.9) - sass (3.2.1) - sass-rails (3.2.5) + sass (3.2.7) + sass-rails (3.2.6) railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) sax-machine (0.0.20) nokogiri (> 0.0.0) - schema_plus (1.0.1) - rails (>= 3.2) + schema_associations (1.0.1) + schema_plus + schema_plus (1.1.1) + rails (>= 3.2, <= 3.2.12) valuable - selenium-webdriver (2.25.0) + selenium-webdriver (2.31.0) childprocess (>= 0.2.5) - libwebsocket (~> 0.1.3) multi_json (~> 1.0) rubyzip - shoulda (3.1.1) - shoulda-context (~> 1.0) - shoulda-matchers (~> 1.2) - shoulda-context (1.0.0) - shoulda-matchers (1.3.0) + websocket (~> 1.0.4) + shoulda (3.3.2) + shoulda-context (~> 1.0.1) + shoulda-matchers (~> 1.4.1) + shoulda-context (1.0.2) + shoulda-matchers (1.4.2) activesupport (>= 3.0.0) - sidekiq (2.4.0) + bourne (~> 1.1.2) + sidekiq (2.7.5) celluloid (~> 0.12.0) - connection_pool (~> 0.9.2) + connection_pool (~> 1.0) multi_json (~> 1) redis (~> 3) redis-namespace - sinatra (1.3.3) - rack (~> 1.3, >= 1.3.6) - rack-protection (~> 1.2) + sinatra (1.3.5) + rack (~> 1.4) + rack-protection (~> 1.3) tilt (~> 1.3, >= 1.3.3) - skinny (0.2.1) - eventmachine (~> 0.12) - thin (~> 1.2) - slim (1.2.2) - temple (~> 0.4.0) + skinny (0.2.3) + eventmachine (~> 1.0.0) + thin (~> 1.5.0) + slim (1.3.6) + temple (~> 0.5.5) tilt (~> 1.3.3) - slim-rails (1.0.3) - actionpack (~> 3.0) - activesupport (~> 3.0) - railties (~> 3.0) - slim (~> 1.0) - spectator-validates_email (0.1.1) - actionpack (>= 3.0.0) + slim-rails (1.1.0) + actionpack (>= 3.0, < 4.1) + activesupport (>= 3.0, < 4.1) + railties (>= 3.0, < 4.1) + slim (~> 1.3) + spectator-validates_email (0.2.0) activemodel (>= 3.0.0) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sqlite3 (1.3.6) + sqlite3 (1.3.7) state_machine (1.1.2) supermodel (0.1.4) activemodel (>= 3.0.0.beta) tag_helper (0.0.3) - temple (0.4.0) - thin (1.4.1) + temple (0.5.5) + thin (1.5.0) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) thor (0.17.0) - tilt (1.3.3) - timers (1.0.1) + tilt (1.3.5) + timers (1.1.0) treetop (1.4.12) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.35) - uglifier (1.3.0) + tzinfo (0.3.37) + uglifier (1.0.4) execjs (>= 0.3.0) - multi_json (~> 1.0, >= 1.0.2) - unicode (0.4.3) - valuable (0.9.6) + multi_json (>= 1.0.2) + valuable (0.9.8) vimeo (1.5.3) httparty (>= 0.4.5) httpclient (>= 2.1.5.2) @@ -439,9 +416,9 @@ GEM oauth (>= 0.4.3) warden (1.2.1) rack (>= 1.0) + websocket (1.0.7) weekdays (1.0.2) - wirble (0.1.3) - xpath (0.1.4) + xpath (1.0.0) nokogiri (~> 1.3) PLATFORMS @@ -453,9 +430,8 @@ DEPENDENCIES airbrake auto_html (= 1.4.2) best_in_place - brcep cancan! - capybara (>= 1.0.1) + capybara (~> 2.0.2) carrierwave (~> 0.7.0) catarse_mailchimp! catarse_moip! @@ -464,10 +440,10 @@ DEPENDENCIES compass-960-plugin (~> 0.10.4) compass-rails (~> 1.0.2) database_cleaner - devise (= 1.5.3) + devise draper enumerate_it - factory_girl_rails (= 1.7.0) + factory_girl_rails feedzirra fog foreman @@ -475,7 +451,7 @@ DEPENDENCIES has_scope has_vimeo_video (~> 0.0.5) http_accept_language - httparty (~> 0.6.1) + httparty (~> 0.8.1) httpclient (= 2.2.5) inherited_resources (= 1.3.1) initjs @@ -483,40 +459,32 @@ DEPENDENCIES kaminari launchy mailcatcher - memoist (~> 0.2.0) - meta_search (= 1.1.3) - mocha (= 0.10.4) - moip! - omniauth (~> 1.1.0) - omniauth-facebook (~> 1.2.0) - omniauth-github (~> 1.0.1) - omniauth-linkedin (~> 0.0.6) - omniauth-openid (~> 1.0.1) - omniauth-twitter (~> 0.0.12) - omniauth-yahoo (~> 0.0.4) - on_the_spot + mocha (~> 0.10.4) + moip (= 2.1.2) + omniauth + omniauth-facebook (= 1.4.0) + omniauth-twitter + paper_trail (~> 2.7.1) pg pg_search postgres-copy rack-timeout - rails (= 3.2.11) + rails (= 3.2.12) rails_autolink (~> 1.0.7) rmagick routing-filter - rspec-rails (~> 2.10.0) + rspec-rails sass-rails (~> 3.2.5) + schema_associations schema_plus - selenium-webdriver shoulda - sidekiq (= 2.4.0) + sidekiq (~> 2.7.5) sinatra slim slim-rails spectator-validates_email state_machine thin - uglifier (>= 1.0.3) - unicode + uglifier (~> 1.0.3) validation_reflection! weekdays - wirble diff --git a/spec/controllers/catarse_moip/payment/moip_controller_spec.rb b/spec/controllers/catarse_moip/payment/moip_controller_spec.rb index 28659e1..f7c98e2 100644 --- a/spec/controllers/catarse_moip/payment/moip_controller_spec.rb +++ b/spec/controllers/catarse_moip/payment/moip_controller_spec.rb @@ -7,7 +7,7 @@ let(:get_token_response){{:status=>:fail, :code=>"171", :message=>"TelefoneFixo do endereço deverá ser enviado obrigatorio", :id=>"201210192052439150000024698931"}} before do - @backer = Factory(:backer, :confirmed => false) + @backer = FactoryGirl.create(:backer, :confirmed => false) controller.stub(:current_user).and_return(@backer.user) ::MoipTransparente::Checkout.any_instance.stub(:get_token).and_return(get_token_response) ::MoipTransparente::Checkout.any_instance.stub(:moip_widget_tag).and_return('
') diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5eef36e..5418a25 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -41,7 +41,7 @@ config.include Devise::TestHelpers, :type => :controller - config.include Factory::Syntax::Methods + config.include FactoryGirl::Syntax::Methods config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with :truncation diff --git a/test/dummy b/test/dummy index 8076877..3ab3f1a 160000 --- a/test/dummy +++ b/test/dummy @@ -1 +1 @@ -Subproject commit 8076877051df9e637f71a9a9fdce399414d41fe6 +Subproject commit 3ab3f1ab8548b9954ed3b0924c858894ed5b7eba From 130b5075db34db8e615b7b93072447ab658c17c4 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Wed, 13 Mar 2013 17:04:06 -0300 Subject: [PATCH 002/102] fixed specs as the MoIP client interface changed --- spec/lib/catarse_moip/processors/moip_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/catarse_moip/processors/moip_spec.rb b/spec/lib/catarse_moip/processors/moip_spec.rb index 7ae22ca..4ef8662 100644 --- a/spec/lib/catarse_moip/processors/moip_spec.rb +++ b/spec/lib/catarse_moip/processors/moip_spec.rb @@ -52,7 +52,7 @@ before do backer.update_attributes :payment_id => nil - MoIP::Client.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) processor.update_backer end @@ -64,7 +64,7 @@ context "with a fake data set that works for some cases" do before do backer.update_attributes :payment_id => nil - MoIP::Client.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) processor.update_backer end From 9ba2b2be228a90071af2393b38e6f2a6ca9df624 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Wed, 13 Mar 2013 21:18:23 -0300 Subject: [PATCH 003/102] just updated gems and catarse source code --- Gemfile | 4 +-- Gemfile.lock | 29 ++++++++++--------- .../payment/notifications_controller.rb | 2 +- test/dummy | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Gemfile b/Gemfile index 281811d..500b70f 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'catarse_paypal_express', git: 'git://github.com/catarse/catarse_paypal_expr #gem 'catarse_moip', git: 'git://github.com/catarse/catarse_moip.git', ref: '98f992428fcd5c4fc6edabd647ace9425b1e578a' # TODO: Check the Catarse_Moip dependency -gem 'moip', '2.1.2' +gem 'moip', git: 'git://github.com/moiplabs/moip-ruby.git' # Decorators gem 'draper' @@ -80,7 +80,7 @@ gem 'has_scope' gem 'spectator-validates_email', require: 'validates_email' gem 'has_vimeo_video', '~> 0.0.5' gem 'enumerate_it' -gem 'httparty', '~> 0.8.1' +gem 'httparty', '~> 0.6.1' # this version is required by moip gem, otherwise payment confirmation will break # Translations gem 'http_accept_language' diff --git a/Gemfile.lock b/Gemfile.lock index a08ca74..c77c145 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,6 +15,15 @@ GIT activemerchant (~> 1.17.0) rails (~> 3.2.6) +GIT + remote: git://github.com/moiplabs/moip-ruby.git + revision: 2b61a77355cb8c5487ff68be0eef8873696a110e + specs: + moip (1.0.2) + activesupport (>= 2.3.2) + httparty (~> 0.6.1) + nokogiri (~> 1.4.3) + GIT remote: git://github.com/ncri/validation_reflection.git revision: 60320e6beb088808fd625a8d958dbd0d2661d494 @@ -124,6 +133,7 @@ GEM compass-rails (1.0.3) compass (>= 0.12.2, < 0.14) connection_pool (1.0.0) + crack (0.1.8) curb (0.7.18) daemons (1.1.9) database_cleaner (0.9.1) @@ -137,14 +147,14 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) request_store (~> 1.0.3) - enumerate_it (1.0.2) + enumerate_it (1.0.3) activesupport (>= 3.0.0) erubis (2.7.0) eventmachine (1.0.3) excon (0.6.6) execjs (1.4.0) multi_json (~> 1.0) - facter (1.6.17) + facter (1.6.18) factory_girl (4.2.0) activesupport (>= 3.0.0) factory_girl_rails (4.2.1) @@ -188,9 +198,8 @@ GEM hashie (1.2.0) hike (1.2.1) http_accept_language (1.0.2) - httparty (0.8.3) - multi_json (~> 1.0) - multi_xml + httparty (0.6.1) + crack (= 0.1.8) httpauth (0.2.0) httpclient (2.2.5) i18n (0.6.4) @@ -234,13 +243,7 @@ GEM mime-types (1.21) mocha (0.10.5) metaclass (~> 0.0.1) - moip (2.1.2) - activesupport (>= 2.3.2) - httparty (~> 0.8.1) - moip - nokogiri (~> 1.4.3) multi_json (1.6.1) - multi_xml (0.5.3) multipart-post (1.2.0) net-scp (1.1.0) net-ssh (>= 2.6.5) @@ -451,7 +454,7 @@ DEPENDENCIES has_scope has_vimeo_video (~> 0.0.5) http_accept_language - httparty (~> 0.8.1) + httparty (~> 0.6.1) httpclient (= 2.2.5) inherited_resources (= 1.3.1) initjs @@ -460,7 +463,7 @@ DEPENDENCIES launchy mailcatcher mocha (~> 0.10.4) - moip (= 2.1.2) + moip! omniauth omniauth-facebook (= 1.4.0) omniauth-twitter diff --git a/app/controllers/catarse_moip/payment/notifications_controller.rb b/app/controllers/catarse_moip/payment/notifications_controller.rb index 0c4e260..3bc658a 100644 --- a/app/controllers/catarse_moip/payment/notifications_controller.rb +++ b/app/controllers/catarse_moip/payment/notifications_controller.rb @@ -4,7 +4,7 @@ module CatarseMoip::Payment class NotificationsController < ApplicationController skip_before_filter :force_http def create - @backer = Backer.find_by_key! params[:id_transacao] + @backer = Backer.find_by_key! params[:id_transacao] @processor = CatarseMoip::Processors::Moip.new @backer @processor.process!(params) return render :nothing => true, :status => 200 diff --git a/test/dummy b/test/dummy index 3ab3f1a..42bfc2d 160000 --- a/test/dummy +++ b/test/dummy @@ -1 +1 @@ -Subproject commit 3ab3f1ab8548b9954ed3b0924c858894ed5b7eba +Subproject commit 42bfc2dbe0780a129e9b13c7262a9fc2ca6644ed From dd50347acecf161930c8453ddccbe7e564933efd Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Tue, 19 Mar 2013 17:05:09 -0300 Subject: [PATCH 004/102] added test for moip_response method. Use the processor method in moip_response to get transaction data --- .../catarse_moip/payment/moip_controller.rb | 10 ++---- .../payment/moip_controller_spec.rb | 31 ++++++++++++++----- test/dummy | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/controllers/catarse_moip/payment/moip_controller.rb b/app/controllers/catarse_moip/payment/moip_controller.rb index 2735819..e36172d 100644 --- a/app/controllers/catarse_moip/payment/moip_controller.rb +++ b/app/controllers/catarse_moip/payment/moip_controller.rb @@ -18,15 +18,9 @@ def moip_response @backer.payment_notifications.create(extra_data: params[:response]) - if not @backer.confirmed and params[:response]['Status'] == 'Autorizado' - @backer.confirm! - end - unless params[:response]['StatusPagamento'] == 'Falha' - @backer.update_attributes({ - payment_id: params[:response]['CodigoMoIP'], - payment_service_fee: params[:response]['TaxaMoIP'].to_f - }) + @processor = CatarseMoip::Processors::Moip.new @backer + @processor.process!(params) end render nothing: true, status: 200 diff --git a/spec/controllers/catarse_moip/payment/moip_controller_spec.rb b/spec/controllers/catarse_moip/payment/moip_controller_spec.rb index f7c98e2..18e3f87 100644 --- a/spec/controllers/catarse_moip/payment/moip_controller_spec.rb +++ b/spec/controllers/catarse_moip/payment/moip_controller_spec.rb @@ -3,16 +3,31 @@ describe CatarseMoip::Payment::MoipController do subject{ response } - describe "POST get_moip_token" do - let(:get_token_response){{:status=>:fail, :code=>"171", :message=>"TelefoneFixo do endereço deverá ser enviado obrigatorio", :id=>"201210192052439150000024698931"}} + let(:get_token_response){{:status=>:fail, :code=>"171", :message=>"TelefoneFixo do endereço deverá ser enviado obrigatorio", :id=>"201210192052439150000024698931"}} + + before do + @backer = FactoryGirl.create(:backer, :confirmed => false) + controller.stub(:current_user).and_return(@backer.user) + ::MoipTransparente::Checkout.any_instance.stub(:get_token).and_return(get_token_response) + ::MoipTransparente::Checkout.any_instance.stub(:moip_widget_tag).and_return('
') + ::MoipTransparente::Checkout.any_instance.stub(:moip_javascript_tag).and_return('\"}" } + its(:body){ should == "{\"get_token_response\":{\"status\":\"fail\",\"code\":\"171\",\"message\":\"TelefoneFixo do endereço deverá ser enviado obrigatorio\",\"id\":\"201210192052439150000024698931\"},\"moip\":\"{}\",\"widget_tag\":\"
\\n
\",\"javascript_tag\":\"\"}" } end end From e66f43675550752c51d84a6487c390552b446db4 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 17 May 2013 11:06:43 -0300 Subject: [PATCH 024/102] migrating old processor specs to controller specs WIP --- .../catarse_moip/moip_controller.rb | 50 +++---- .../catarse_moip/moip_controller_spec.rb | 129 ++++++++++++++++++ 2 files changed, 155 insertions(+), 24 deletions(-) diff --git a/app/controllers/catarse_moip/moip_controller.rb b/app/controllers/catarse_moip/moip_controller.rb index 32a0188..33d8423 100644 --- a/app/controllers/catarse_moip/moip_controller.rb +++ b/app/controllers/catarse_moip/moip_controller.rb @@ -2,6 +2,8 @@ module CatarseMoip class MoipController < ApplicationController + attr_accessor :backer + class TransactionStatus < ::EnumerateIt::Base associate_values( :authorized => 1, @@ -44,8 +46,8 @@ def review def moip_response @backer = PaymentEngines.find_payment id: params[:id], user_id: current_user.id - PaymentEngines.create_payment_notification backer_id: @backer.id, extra_data: params[:response] - @backer.waiting! if @backer.pending? + PaymentEngines.create_payment_notification backer_id: backer.id, extra_data: params[:response] + backer.waiting! if backer.pending? process_moip_message params unless params[:response]['StatusPagamento'] == 'Falha' @@ -62,40 +64,40 @@ def get_moip_token @moip = ::MoipTransparente::Checkout.new invoice = { - razao: "Apoio para o projeto '#{@backer.project.name}'", - id: @backer.key, - total: @backer.value.to_s, + razao: "Apoio para o projeto '#{backer.project.name}'", + id: backer.key, + total: backer.value.to_s, acrescimo: '0.00', desconto: '0.00', cliente: { - id: @backer.user.id, - nome: @backer.payer_name, - email: @backer.payer_email, - logradouro: "#{@backer.address_street}, #{@backer.address_number}", - complemento: @backer.address_complement, - bairro: @backer.address_neighbourhood, - cidade: @backer.address_city, - uf: @backer.address_state, - cep: @backer.address_zip_code, - telefone: @backer.address_phone_number + id: backer.user.id, + nome: backer.payer_name, + email: backer.payer_email, + logradouro: "#{backer.address_street}, #{backer.address_number}", + complemento: backer.address_complement, + bairro: backer.address_neighbourhood, + cidade: backer.address_city, + uf: backer.address_state, + cep: backer.address_zip_code, + telefone: backer.address_phone_number } } response = @moip.get_token(invoice) - session[:thank_you_id] = @backer.project.id + session[:thank_you_id] = backer.project.id - @backer.update_column :payment_token, response[:token] if response and response[:token] + backer.update_column :payment_token, response[:token] if response and response[:token] render json: { get_token_response: response, moip: @moip, widget_tag: @moip.widget_tag('checkoutSuccessful', 'checkoutFailure'), javascript_tag: @moip.javascript_tag } end def update_backer - response = ::MoIP.query(@backer.payment_token) + response = ::MoIP.query(backer.payment_token) if response && response["Autorizacao"] pagamento = response["Autorizacao"]["Pagamento"] pagamento = pagamento.first unless pagamento.respond_to?(:key) - @backer.update_attributes({ + backer.update_attributes({ :payment_id => pagamento["CodigoMoIP"], :payment_choice => pagamento["FormaPagamento"], :payment_service_fee => pagamento["TaxaMoIP"] @@ -104,15 +106,15 @@ def update_backer end def process_moip_message params - update_backer if @backer.payment_id.nil? - PaymentEngines.create_payment_notification backer_id: @backer.id, extra_data: JSON.parse(params.to_json.force_encoding('iso-8859-1').encode('utf-8')) + update_backer if backer.payment_id.nil? + PaymentEngines.create_payment_notification backer_id: backer.id, extra_data: JSON.parse(params.to_json.force_encoding('iso-8859-1').encode('utf-8')) case params[:status_pagamento].to_i when TransactionStatus::AUTHORIZED - @backer.confirm! unless @backer.confirmed? + backer.confirm! unless backer.confirmed? when TransactionStatus::WRITTEN_BACK, TransactionStatus::REFUNDED - @backer.refund! unless @backer.refunded? + backer.refund! unless backer.refunded? when TransactionStatus::CANCELED - @backer.cancel! unless @backer.canceled? + backer.cancel! unless backer.canceled? end end end diff --git a/spec/controllers/catarse_moip/moip_controller_spec.rb b/spec/controllers/catarse_moip/moip_controller_spec.rb index 72096f8..6561fad 100644 --- a/spec/controllers/catarse_moip/moip_controller_spec.rb +++ b/spec/controllers/catarse_moip/moip_controller_spec.rb @@ -107,4 +107,133 @@ its(:status){ should == 200 } its(:body){ should == "{\"get_token_response\":{\"status\":\"fail\",\"code\":\"171\",\"message\":\"TelefoneFixo do endereço deverá ser enviado obrigatorio\",\"id\":\"201210192052439150000024698931\"},\"moip\":\"{}\",\"widget_tag\":\"
\\n
\",\"javascript_tag\":\"\"}" } end + + describe "#update_backer" do + before do + controller.stub(:backer).and_return(backer) + backer.stub(:payment_token).and_return('token') + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + end + + context "with no response from moip" do + let(:moip_query_response) { nil } + before{ backer.should_not_receive(:update_attributes) } + it("should never call update attributes"){ controller.update_backer } + end + + context "with an incomplete transaction" do + let(:moip_query_response) do + {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso"} + end + before{ backer.should_not_receive(:update_attributes) } + it("should never call update attributes"){ controller.update_backer } + end + + context "with a real data set that works for some cases" do + let(:moip_query_response) do + {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso", "Autorizacao"=>{"Pagador"=>{"Nome"=>"juliana.giopato@hotmail.com", "Email"=>"juliana.giopato@hotmail.com"}, "EnderecoCobranca"=>{"Logradouro"=>"Rua sócrates abraão ", "Numero"=>"16.0", "Complemento"=>"casa 19", "Bairro"=>"Campo Limpo", "CEP"=>"05782-470", "Cidade"=>"São Paulo", "Estado"=>"SP", "Pais"=>"BRA", "TelefoneFixo"=>"1184719963"}, "Recebedor"=>{"Nome"=>"Catarse", "Email"=>"financeiro@catarse.me"}, "Pagamento"=>[{"Data"=>"2012-10-17T13:06:07.000-03:00", "DataCredito"=>"2012-10-19T00:00:00.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"1.34", "ValorLiquido"=>"48.66", "FormaPagamento"=>"BoletoBancario", "InstituicaoPagamento"=>"Bradesco", "Status"=>"Autorizado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5258"}, {"Data"=>"2012-10-17T13:05:49.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"3.09", "ValorLiquido"=>"46.91", "FormaPagamento"=>"CartaoDebito", "InstituicaoPagamento"=>"Visa", "Status"=>"Iniciado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5248"}]}} + end + before do + payment = moip_query_response["Autorizacao"]["Pagamento"].first + backer.should_receive(:update_attributes).with({ + payment_id: payment["CodigoMoIP"], + payment_choice: payment["FormaPagamento"], + payment_service_fee: payment["TaxaMoIP"] + }) + end + it("should call update attributes"){ controller.update_backer } + end + end + + describe "#process_moip_message" do + before do + controller.stub(:backer).and_return(backer) + backer.stub(:confirmed?).and_return(false) + backer.stub(:confirm!) + controller.stub(:update_backer) + end + + context "when we already have the payment_id in backers table" do + before do + backer.stub(:payment_id).and_return('test') + controller.should_not_receive(:update_backer) + end + + it 'should never call update_backer' do + controller.process_moip_message post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::MoipController::TransactionStatus::AUTHORIZED}) + end + end + + context "when we still do not have the payment_id in backers table" do + before do + backer.stub(:payment_id).and_return(nil) + controller.should_receive(:update_backer) + end + + it 'should call update_backer on the processor' do + controller.process_moip_message post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::MoipController::TransactionStatus::AUTHORIZED}) + end + end + + context "when there is a written back request" do + let(:backer){ create(:backer, state: 'confirmed') } + before do + processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK}) + end + + it 'should mark refunded to true' do + backer.reload.refunded?.should be_true + end + + it 'should create a proper payment_notification' do + backer.reload.payment_notifications.size.should == 1 + backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK) + end + end + + context "when there is an authorized request" do + before do + processor.process!(Hashie::Mash.new({"id_transacao"=>"#{backer.key}", "valor"=>"5000", "status_pagamento"=>"1", "cod_moip"=>"13255258", "forma_pagamento"=>"73", "tipo_pagamento"=>"BoletoBancario", "parcelas"=>"1", "recebedor_login"=>"softa", "email_consumidor"=>"juliana.giopato@hotmail.com", "action"=>"create", "controller"=>"catarse_moip/payment/notifications"})) + end + + it 'should confirm the backer' do + backer.reload.confirmed.should be_true + end + end + + context "when there is an authorized request" do + before do + processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED}) + end + + it 'should mark refunded to true' do + backer.reload.refunded.should be_false + end + + it 'should create a proper payment_notification' do + backer.reload.payment_notifications.size.should == 1 + backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED) + end + + it 'should confirm the backer' do + backer.reload.confirmed.should be_true + end + end + + context "when there is a refund request" do + let(:backer){ create(:backer, state: 'confirmed') } + before do + processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED}) + end + + it 'should mark refunded to true' do + backer.reload.refunded?.should be_true + end + + it 'should create a proper payment_notification' do + backer.reload.payment_notifications.size.should == 1 + backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED) + end + end + end end From 52fd0ea90778903994869394e44eb99fc4013120 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 17 May 2013 11:23:25 -0300 Subject: [PATCH 025/102] fixed specs and removed old processor specs --- .../catarse_moip/moip_controller_spec.rb | 51 ++---- spec/lib/catarse_moip/processors/moip_spec.rb | 164 ------------------ 2 files changed, 11 insertions(+), 204 deletions(-) delete mode 100644 spec/lib/catarse_moip/processors/moip_spec.rb diff --git a/spec/controllers/catarse_moip/moip_controller_spec.rb b/spec/controllers/catarse_moip/moip_controller_spec.rb index 6561fad..a3bd8e4 100644 --- a/spec/controllers/catarse_moip/moip_controller_spec.rb +++ b/spec/controllers/catarse_moip/moip_controller_spec.rb @@ -175,64 +175,35 @@ end end - context "when there is a written back request" do - let(:backer){ create(:backer, state: 'confirmed') } + context "when there is a written back request and backer is not refunded" do before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK}) + backer.stub(:refunded?).and_return(false) + backer.should_receive(:refund!) end - it 'should mark refunded to true' do - backer.reload.refunded?.should be_true - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK) - end - end - - context "when there is an authorized request" do - before do - processor.process!(Hashie::Mash.new({"id_transacao"=>"#{backer.key}", "valor"=>"5000", "status_pagamento"=>"1", "cod_moip"=>"13255258", "forma_pagamento"=>"73", "tipo_pagamento"=>"BoletoBancario", "parcelas"=>"1", "recebedor_login"=>"softa", "email_consumidor"=>"juliana.giopato@hotmail.com", "action"=>"create", "controller"=>"catarse_moip/payment/notifications"})) - end - - it 'should confirm the backer' do - backer.reload.confirmed.should be_true + it 'should call refund!' do + controller.process_moip_message post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::MoipController::TransactionStatus::WRITTEN_BACK}) end end context "when there is an authorized request" do before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED}) + backer.should_receive(:confirm!) end - it 'should mark refunded to true' do - backer.reload.refunded.should be_false - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED) - end - - it 'should confirm the backer' do - backer.reload.confirmed.should be_true + it 'should call confirm!' do + controller.process_moip_message post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::MoipController::TransactionStatus::AUTHORIZED}) end end context "when there is a refund request" do - let(:backer){ create(:backer, state: 'confirmed') } before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED}) + backer.stub(:refunded?).and_return(false) + backer.should_receive(:refund!) end it 'should mark refunded to true' do - backer.reload.refunded?.should be_true - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED) + controller.process_moip_message post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::MoipController::TransactionStatus::REFUNDED}) end end end diff --git a/spec/lib/catarse_moip/processors/moip_spec.rb b/spec/lib/catarse_moip/processors/moip_spec.rb deleted file mode 100644 index 7cd17e3..0000000 --- a/spec/lib/catarse_moip/processors/moip_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -describe CatarseMoip::Processors::Moip do - let(:post_moip_params) do - { - :id_transacao => 'ABCD', - :valor => 2190, #=> R$ 21,90 - :status_pagamento => 3, - :cod_moip => 12345123, - :forma_pagamento => 1, - :tipo_pagamento => 'CartaoDeCredito', - :email_consumidor => 'some@email.com' - } - end - - let(:moip_query_response) do - { - "ID"=>"201109300946542390000012428473", "Status"=>"Sucesso", - "Autorizacao"=>{ - "Pagador"=>{ - "Nome"=>"Lorem Ipsum", "Email"=>"some@email.com" - }, - "EnderecoCobranca"=>{ - "Logradouro"=>"Some Address ,999", "Numero"=>"999", - "Complemento"=>"Address A", "Bairro"=>"Hello World", "CEP"=>"99999-000", - "Cidade"=>"Some City", "Estado"=>"MG", "Pais"=>"BRA", - "TelefoneFixo"=>"(31)3666-6666" - }, - "Recebedor"=>{ - "Nome"=>"Happy Guy", "Email"=>"happy@email.com" - }, - "Pagamento"=>{ - "Data"=>"2011-09-30T09:33:37.000-03:00", "TotalPago"=>"999.00", "TaxaParaPagador"=>"0.00", - "TaxaMoIP"=>"19.37", "ValorLiquido"=>"979.63", "FormaPagamento"=>"BoletoBancario", - "InstituicaoPagamento"=>"Itau", "Status"=>"BoletoImpresso", "CodigoMoIP"=>"0000.0728.5285" - } - } - } - end - - let(:extra_data){ {"id_transacao"=>backer.key, "valor"=>2190, "cod_moip"=>12345123, "forma_pagamento"=>1, "tipo_pagamento"=>"CartaoDeCredito", "email_consumidor"=>"some@email.com"} } - let(:backer){ create(:backer) } - let(:processor){ CatarseMoip::Processors::Moip.new backer } - - describe "#update_backer" do - before do - backer.update_attributes :payment_id => nil - MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) - processor.update_backer - end - - context "with no response from moip" do - let(:moip_query_response) { nil } - it("should not assign payment_id"){ backer.payment_id.should be_nil } - end - - context "with an incomplete transaction" do - let(:moip_query_response) do - {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso"} - end - it("should not assign payment_id"){ backer.payment_id.should be_nil } - end - - context "with a real data set that works for some cases" do - let(:moip_query_response) do - {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso", "Autorizacao"=>{"Pagador"=>{"Nome"=>"juliana.giopato@hotmail.com", "Email"=>"juliana.giopato@hotmail.com"}, "EnderecoCobranca"=>{"Logradouro"=>"Rua sócrates abraão ", "Numero"=>"16.0", "Complemento"=>"casa 19", "Bairro"=>"Campo Limpo", "CEP"=>"05782-470", "Cidade"=>"São Paulo", "Estado"=>"SP", "Pais"=>"BRA", "TelefoneFixo"=>"1184719963"}, "Recebedor"=>{"Nome"=>"Catarse", "Email"=>"financeiro@catarse.me"}, "Pagamento"=>[{"Data"=>"2012-10-17T13:06:07.000-03:00", "DataCredito"=>"2012-10-19T00:00:00.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"1.34", "ValorLiquido"=>"48.66", "FormaPagamento"=>"BoletoBancario", "InstituicaoPagamento"=>"Bradesco", "Status"=>"Autorizado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5258"}, {"Data"=>"2012-10-17T13:05:49.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"3.09", "ValorLiquido"=>"46.91", "FormaPagamento"=>"CartaoDebito", "InstituicaoPagamento"=>"Visa", "Status"=>"Iniciado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5248"}]}} - end - it("should assign payment_id"){ backer.payment_id.should == moip_query_response["Autorizacao"]["Pagamento"][0]["CodigoMoIP"] } - it("should assign payment_choice"){ backer.payment_choice.should == moip_query_response["Autorizacao"]["Pagamento"][0]["FormaPagamento"] } - it("should assign payment_service_fee"){ backer.payment_service_fee.to_s.should == moip_query_response["Autorizacao"]["Pagamento"][0]["TaxaMoIP"] } - end - end - - describe "#process!" do - before do - processor.stub(:update_backer) - end - - context "when we already have the payment_id in backers table" do - before do - backer.update_attributes :payment_id => 'test' - processor.should_not_receive(:update_backer) - end - - it 'should call update_backer on the processor' do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED}) - end - end - - context "when we still do not have the payment_id in backers table" do - before do - backer.update_attributes :payment_id => nil - processor.should_receive(:update_backer) - end - - it 'should call update_backer on the processor' do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED}) - end - end - - context "when there is a written back request" do - let(:backer){ create(:backer, state: 'confirmed') } - before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK}) - end - - it 'should mark refunded to true' do - backer.reload.refunded?.should be_true - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::WRITTEN_BACK) - end - end - - context "when there is an authorized request" do - before do - processor.process!(Hashie::Mash.new({"id_transacao"=>"#{backer.key}", "valor"=>"5000", "status_pagamento"=>"1", "cod_moip"=>"13255258", "forma_pagamento"=>"73", "tipo_pagamento"=>"BoletoBancario", "parcelas"=>"1", "recebedor_login"=>"softa", "email_consumidor"=>"juliana.giopato@hotmail.com", "action"=>"create", "controller"=>"catarse_moip/payment/notifications"})) - end - - it 'should confirm the backer' do - backer.reload.confirmed.should be_true - end - end - - context "when there is an authorized request" do - before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED}) - end - - it 'should mark refunded to true' do - backer.reload.refunded.should be_false - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::AUTHORIZED) - end - - it 'should confirm the backer' do - backer.reload.confirmed.should be_true - end - end - - context "when there is a refund request" do - let(:backer){ create(:backer, state: 'confirmed') } - before do - processor.process! post_moip_params.merge!({:id_transacao => backer.key, :status_pagamento => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED}) - end - - it 'should mark refunded to true' do - backer.reload.refunded?.should be_true - end - - it 'should create a proper payment_notification' do - backer.reload.payment_notifications.size.should == 1 - backer.reload.payment_notifications.first.extra_data.should == extra_data.merge("status_pagamento" => CatarseMoip::Processors::Moip::TransactionStatus::REFUNDED) - end - end - end -end From 96e1b4788dab4aa32728f97b685864c082e24f75 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Mon, 20 May 2013 09:59:57 -0300 Subject: [PATCH 026/102] finished removing payment namespace --- .../{payment => }/moip/review.html.slim | 2 +- config/initializers/register.rb | 2 +- config/routes.rb | 20 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) rename app/views/catarse_moip/{payment => }/moip/review.html.slim (98%) diff --git a/app/views/catarse_moip/payment/moip/review.html.slim b/app/views/catarse_moip/moip/review.html.slim similarity index 98% rename from app/views/catarse_moip/payment/moip/review.html.slim rename to app/views/catarse_moip/moip/review.html.slim index ea28d62..45a7008 100644 --- a/app/views/catarse_moip/payment/moip/review.html.slim +++ b/app/views/catarse_moip/moip/review.html.slim @@ -102,4 +102,4 @@ -== javascript_include_tag js_payment_moip_index_path +== javascript_include_tag js_moip_index_path diff --git a/config/initializers/register.rb b/config/initializers/register.rb index 5550dd7..3d83528 100644 --- a/config/initializers/register.rb +++ b/config/initializers/register.rb @@ -1,5 +1,5 @@ begin - PaymentEngines.register({name: 'moip', review_path: ->(backer){ CatarseMoip::Engine.routes.url_helpers.review_payment_moip_path(backer) }, locale: 'pt'}) + PaymentEngines.register({name: 'moip', review_path: ->(backer){ CatarseMoip::Engine.routes.url_helpers.review_moip_path(backer) }, locale: 'pt'}) rescue Exception => e puts "Error while registering payment engine: #{e}" end diff --git a/config/routes.rb b/config/routes.rb index abb8b61..e6bff4b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,15 +1,13 @@ CatarseMoip::Engine.routes.draw do - namespace :payment do - resources :moip, only: [] do - collection do - post 'notifications' => "moip#create_notification" - get 'js' - end - member do - match :moip_response - match :review - match :get_moip_token - end + resources :moip, only: [], path: 'payment/moip' do + collection do + post 'notifications' => "moip#create_notification" + get 'js' + end + member do + match :moip_response + match :review + match :get_moip_token end end end From b9bdd34f69ceec07459bfc0f0ed07861bae9fc60 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Mon, 20 May 2013 10:42:03 -0300 Subject: [PATCH 027/102] updated README and added travis config --- .travis.yml | 13 +++++++++++++ README.md | 26 +------------------------- 2 files changed, 14 insertions(+), 25 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..47b1c82 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +rvm: + - 1.9.3 + +before_script: + - "psql -c 'create role catarse SUPERUSER LOGIN;' postgres" + - "psql -c 'create database catarse_test;' -U catarse postgres" + +script: + - "bundle exec rspec spec" + +branches: + only: + - master diff --git a/README.md b/README.md index f87991c..6ee2507 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CatarseMoip -Catarse moip integration with [Catarse](http://github.com/danielweinmann/catarse) crowdfunding platform +Catarse moip integration with [Catarse](http://github.com/catarse/catarse) crowdfunding platform ## Installation @@ -30,30 +30,6 @@ In Rails console, run this: Configuration.create!(name: "moip_token", value: "TOKEN") Configuration.create!(name: "moip_key", value: "KEY") -## Development environment setup - -Clone the repository: - - $ git clone git://github.com/devton/catarse_moip.git - -Add the catarse code into test/dummy: - - $ git submodule add git://github.com/danielweinmann/catarse.git test/dummy - -Copy the Catarse's gems to Gemfile: - - $ cat test/dummy/Gemfile >> Gemfile - -And then execute: - - $ bundle - -## Troubleshooting in development environment - -Remove the admin folder from test/dummy application to prevent a weird active admin bug: - - $ rm -rf test/dummy/app/admin - ## Contributing 1. Fork it From 006e81fa6d3ccba4117dd77f9ad6b33469bc150a Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Mon, 20 May 2013 10:44:03 -0300 Subject: [PATCH 028/102] added status image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ee2507..05d37f2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CatarseMoip +# CatarseMoip [![Build Status](https://travis-ci.org/catarse/catarse_moip.png)](https://travis-ci.org/catarse/catarse_moip) Catarse moip integration with [Catarse](http://github.com/catarse/catarse) crowdfunding platform From 501467bfbc4348af7a9a7bff41b4633cd6895717 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Mon, 20 May 2013 10:48:03 -0300 Subject: [PATCH 029/102] fixed user in database.yml --- test/dummy/config/database.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index 4035818..5a5c41e 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -13,7 +13,7 @@ development: encoding: utf-8 database: catarse_development pool: 5 - username: diogo + username: catarse # Connect on a TCP socket. Omitted by default since the client uses a @@ -39,7 +39,7 @@ test: &test encoding: utf-8 database: catarse_test pool: 5 - username: diogo + username: catarse production: @@ -47,4 +47,4 @@ production: encoding: utf-8 database: catarse_production pool: 5 - username: diogo + username: catarse From c056de1ec3e8b9a3c0035fe6c8e2976b368f1c5b Mon Sep 17 00:00:00 2001 From: The Crab Date: Wed, 22 May 2013 13:00:11 +0200 Subject: [PATCH 030/102] Update libxml-ruby version to the latest I could not install or proceed with my catarse install due to this version failing on OSX. --- catarse_moip.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catarse_moip.gemspec b/catarse_moip.gemspec index 0fd7cce..a4c0715 100644 --- a/catarse_moip.gemspec +++ b/catarse_moip.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| s.test_files = s.files.grep(%r{^(test|spec|features)/}) s.add_dependency "rails", "~> 3.2.13" - s.add_dependency('libxml-ruby', '~> 2.3.3') + s.add_dependency('libxml-ruby', '~> 2.6.0') s.add_development_dependency "rspec-rails" s.add_development_dependency "factory_girl_rails" From 8a5436132da8803bfff3df5a233bede502de35e3 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Fri, 24 May 2013 12:21:33 -0300 Subject: [PATCH 031/102] removed CatarseMoip::Checkout module --- lib/catarse_moip.rb | 2 - lib/catarse_moip/checkout/checkout.rb | 220 -------------------------- lib/catarse_moip/checkout/config.rb | 29 ---- 3 files changed, 251 deletions(-) delete mode 100644 lib/catarse_moip/checkout/checkout.rb delete mode 100644 lib/catarse_moip/checkout/config.rb diff --git a/lib/catarse_moip.rb b/lib/catarse_moip.rb index 9a9aed6..203c85f 100644 --- a/lib/catarse_moip.rb +++ b/lib/catarse_moip.rb @@ -1,6 +1,4 @@ require "catarse_moip/engine" -require "catarse_moip/checkout/config" -require "catarse_moip/checkout/checkout" #require "moip" module CatarseMoip diff --git a/lib/catarse_moip/checkout/checkout.rb b/lib/catarse_moip/checkout/checkout.rb deleted file mode 100644 index 6a6b79e..0000000 --- a/lib/catarse_moip/checkout/checkout.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'net/https' -require 'uri' -require 'xml' -require 'base64' - -module CatarseMoip - module Checkout - class Checkout - - def javascript_tag - "" - end - - def widget_tag(success, fail) - "
-
" - end - - def get_token(invoice) - doc = XML::Document.new - doc.root = XML::Node.new('EnviarInstrucao') - - unica = XML::Node.new('InstrucaoUnica') - unica['TipoValidacao'] = 'Transparente' - - - description = XML::Node.new('Razao') - description << invoice[:razao] - unica << description - - id_invoice = XML::Node.new('IdProprio') - id_invoice << invoice[:id] - unica << id_invoice - - valores = XML::Node.new('Valores') - - valor = XML::Node.new('Valor') - valor['moeda'] = 'BRL' - valor << invoice[:total] - - valores << valor - - - valor = XML::Node.new('Acrescimo') - valor['moeda'] = 'BRL' - valores << invoice[:acrescimo] || '0.00' - - - valor = XML::Node.new('Deducao') - valor['moeda'] = 'BRL' - valores << invoice[:desconto] || '0.00' - - unica << valores - - pagador = XML::Node.new('Pagador') - nome = XML::Node.new('Nome') - nome << invoice[:cliente][:nome] - pagador << nome - - email = XML::Node.new('Email') - email << invoice[:cliente][:email] - pagador << email - - id = XML::Node.new('IdPagador') - id << invoice[:cliente][:id] - pagador << id - - endereco = XML::Node.new('EnderecoCobranca') - - logradouro = XML::Node.new('Logradouro') - logradouro << invoice[:cliente][:logradouro] - endereco << logradouro - - numero = XML::Node.new('Numero') - numero_or_default = invoice[:cliente][:numero] || '0' - numero << numero_or_default - endereco << numero - - complemento = XML::Node.new('Complemento') - complemento << invoice[:cliente][:complemento] - endereco << complemento - - bairro = XML::Node.new('Bairro') - bairro << invoice[:cliente][:bairro] - endereco << bairro - - cidade = XML::Node.new('Cidade') - cidade << invoice[:cliente][:cidade] - endereco << cidade - - estado = XML::Node.new('Estado') - estado << invoice[:cliente][:uf] - endereco << estado - - pais = XML::Node.new('Pais') - pais_or_default = invoice[:cliente][:pais] || 'BRA' - pais << pais_or_default - endereco << pais - - cep = XML::Node.new('CEP') - cep << invoice[:cliente][:cep] - endereco << cep - - telefone = XML::Node.new('TelefoneFixo') - telefone << invoice[:cliente][:telefone] - endereco << telefone - - pagador << endereco - unica << pagador - - if invoice[:parcelamentos] - parcelamentos = XML::Node.new('Parcelamentos') - invoice[:parcelamentos].each do |parcelamento_item| - parcelamento = XML::Node.new('Parcelamento') - minimo = XML::Node.new('MinimoParcelas') - minimo << parcelamento_item[:minimo] - parcelamento << minimo - - maximo = XML::Node.new('MaximoParcelas') - maximo << parcelamento_item[:maximo] - parcelamento << maximo - - if parcelamento_item[:repassar] - repassar = XML::Node.new('Repassar') - repassar << '1' - parcelamento << repassar - else - juros = XML::Node.new('Juros') - juros << parcelamento_item[:juros] - parcelamento << juros - end - parcelamentos << parcelamento - end - - unica << parcelamentos - end - - comissoes = XML::Node.new('Comissoes') - if invoice[:comissoes] - invoice[:comissoes].each do |comissao_item| - comissionamento = XML::Node.new('Comissionamento') - razao = XML::Node.new('Razao') - razao << comissao_item[:razao] || invoice[:razao] - comissionamento << razao - - comissionado = XML::Node.new("Comissionado") - login_moip = XML::Node.new("LoginMoIP") - login_moip << comissao_item[:login_moip] - comissionado << login_moip - comissionamento << comissionado - - valor = XML::Node.new("ValorFixo") - valor << comissao_item[:valor] - comissionamento << valor - comissoes << comissionamento - end - end - - if invoice[:pagador_taxas] - taxas = XML::Node.new('PagadorTaxa') - login_moip = XML::Node.new("LoginMoIP") - login_moip << invoice[:pagador_taxas] - taxas << login_moip - comissoes << taxas - end - - unica << comissoes - - doc.root << unica - - parser = XML::Parser.string(post_data(doc.to_s(:encoding => XML::Encoding::ISO_8859_1))) - - dom = parser.parse - resposta = dom.find('./Resposta').first - if resposta.find('Status')[0].content == 'Sucesso' - @token = resposta.find('Token')[0].content - return {:status => :ok, :token => resposta.find('Token')[0].content, :id => resposta.find('ID')[0].content} - elsif resposta.find('Status')[0].content == 'Falha' - return {:status => :fail, :code => resposta.find('Erro')[0]['Codigo'], :message => resposta.find('Erro')[0].content, :id => resposta.find('ID')[0].content } - end - end - - private - - def get_token_url - if ::CatarseMoip::Checkout::Config.test? - return "https://desenvolvedor.moip.com.br/sandbox/ws/alpha/EnviarInstrucao/Unica" - else - return "https://www.moip.com.br/ws/alpha/EnviarInstrucao/Unica" - end - end - - def get_javascript_url - if ::CatarseMoip::Checkout::Config.test? - return "https://desenvolvedor.moip.com.br/sandbox/transparente/MoipWidget-v2.js" - else - return "https://www.moip.com.br/transparente/MoipWidget-v2.js" - end - end - - def post_data(xml) - uri = URI.parse(get_token_url) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - #http.set_debug_output $stderr if Moip::Config.test? - - request = Net::HTTP::Post.new(uri.path) - request.basic_auth ::CatarseMoip::Checkout::Config.access_token, ::CatarseMoip::Checkout::Config.access_key - - request.body = xml - response = http.start {|r| r.request request } - response.body - end - end - end -end diff --git a/lib/catarse_moip/checkout/config.rb b/lib/catarse_moip/checkout/config.rb deleted file mode 100644 index d8ed506..0000000 --- a/lib/catarse_moip/checkout/config.rb +++ /dev/null @@ -1,29 +0,0 @@ -module CatarseMoip - module Checkout - class Config - def self.access_token - @access_token - end - - def self.access_token=(value) - @access_token = value - end - - def self.access_key - @access_key - end - - def self.access_key=(value) - @access_key = value - end - - def self.test? - @test || false - end - - def self.test=(test) - @test = test - end - end - end -end From 6f05a2f42cf368d813ac8e15216d4745cf84fdd3 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Wed, 29 May 2013 16:51:07 -0300 Subject: [PATCH 032/102] reduced the number of payment notifications (we had a duplicate call in moip_response). Move the backer.waiting status change to process_moip_message. Put every backer update inside a transaction with pessimistic locking. --- Gemfile.lock | 4 +- .../catarse_moip/moip_controller.rb | 47 ++++++++++--------- .../catarse_moip/moip_controller_spec.rb | 26 ++++++++-- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b12a581..a3a1534 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ PATH remote: . specs: catarse_moip (0.1.1) - libxml-ruby (~> 2.3.3) + libxml-ruby (~> 2.6.0) rails (~> 3.2.13) GEM @@ -63,7 +63,7 @@ GEM i18n (0.6.1) journey (1.0.4) json (1.8.0) - libxml-ruby (2.3.3) + libxml-ruby (2.6.0) mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) diff --git a/app/controllers/catarse_moip/moip_controller.rb b/app/controllers/catarse_moip/moip_controller.rb index 33d8423..8beff30 100644 --- a/app/controllers/catarse_moip/moip_controller.rb +++ b/app/controllers/catarse_moip/moip_controller.rb @@ -46,11 +46,7 @@ def review def moip_response @backer = PaymentEngines.find_payment id: params[:id], user_id: current_user.id - PaymentEngines.create_payment_notification backer_id: backer.id, extra_data: params[:response] - backer.waiting! if backer.pending? - - process_moip_message params unless params[:response]['StatusPagamento'] == 'Falha' - + process_moip_message params[:response] unless params[:response]['StatusPagamento'] == 'Falha' render nothing: true, status: 200 end @@ -92,29 +88,38 @@ def get_moip_token render json: { get_token_response: response, moip: @moip, widget_tag: @moip.widget_tag('checkoutSuccessful', 'checkoutFailure'), javascript_tag: @moip.javascript_tag } end - def update_backer - response = ::MoIP.query(backer.payment_token) - if response && response["Autorizacao"] - pagamento = response["Autorizacao"]["Pagamento"] - pagamento = pagamento.first unless pagamento.respond_to?(:key) + def update_backer params = nil + unless params && params["CodigoMoIP"] && params["TaxaMoIP"] + response = ::MoIP.query(backer.payment_token) + if response && response["Autorizacao"] + params = response["Autorizacao"]["Pagamento"] + params = params.first unless params.respond_to?(:key) + end + end + + if params backer.update_attributes({ - :payment_id => pagamento["CodigoMoIP"], - :payment_choice => pagamento["FormaPagamento"], - :payment_service_fee => pagamento["TaxaMoIP"] + :payment_id => params["CodigoMoIP"], + :payment_choice => params["FormaPagamento"], + :payment_service_fee => params["TaxaMoIP"] }) end end def process_moip_message params - update_backer if backer.payment_id.nil? PaymentEngines.create_payment_notification backer_id: backer.id, extra_data: JSON.parse(params.to_json.force_encoding('iso-8859-1').encode('utf-8')) - case params[:status_pagamento].to_i - when TransactionStatus::AUTHORIZED - backer.confirm! unless backer.confirmed? - when TransactionStatus::WRITTEN_BACK, TransactionStatus::REFUNDED - backer.refund! unless backer.refunded? - when TransactionStatus::CANCELED - backer.cancel! unless backer.canceled? + backer.with_lock do + update_backer if backer.payment_id.nil? + case params[:status_pagamento].to_i + when TransactionStatus::AUTHORIZED + backer.confirm! unless backer.confirmed? + when TransactionStatus::WRITTEN_BACK, TransactionStatus::REFUNDED + backer.refund! unless backer.refunded? + when TransactionStatus::CANCELED + backer.cancel! unless backer.canceled? + else + backer.waiting! if backer.pending? + end end end end diff --git a/spec/controllers/catarse_moip/moip_controller_spec.rb b/spec/controllers/catarse_moip/moip_controller_spec.rb index a3bd8e4..eef1cfd 100644 --- a/spec/controllers/catarse_moip/moip_controller_spec.rb +++ b/spec/controllers/catarse_moip/moip_controller_spec.rb @@ -37,6 +37,7 @@ ::MoipTransparente::Checkout.any_instance.stub(:as_json).and_return('{}') PaymentEngines.stub(:find_payment).and_return(backer) PaymentEngines.stub(:create_payment_notification) + backer.stub(:with_lock).and_yield end describe "POST create_notification" do @@ -112,12 +113,27 @@ before do controller.stub(:backer).and_return(backer) backer.stub(:payment_token).and_return('token') - MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + end + + context "with parameters containing CodigoMoIP and TaxaMoIP" do + let(:payment){ {"Status" => "Autorizado","Codigo" => "0","CodigoRetorno" => "0","TaxaMoIP" => "1.54","StatusPagamento" => "Sucesso","CodigoMoIP" => "18093844","Mensagem" => "Requisição processada com sucesso","TotalPago" => "25.00","url" => "https => //www.moip.com.br/Instrucao.do?token=R2W0N123E005F2A911V6O2I0Y3S7M4J853H0S0F0T0D044T8F4H4E9G0I3W8"} } + before do + MoIP.should_not_receive(:query) + backer.should_receive(:update_attributes).with({ + payment_id: payment["CodigoMoIP"], + payment_choice: payment["FormaPagamento"], + payment_service_fee: payment["TaxaMoIP"] + }) + end + it("should call update attributes but not call MoIP.query"){ controller.update_backer payment } end context "with no response from moip" do let(:moip_query_response) { nil } - before{ backer.should_not_receive(:update_attributes) } + before do + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + backer.should_not_receive(:update_attributes) + end it("should never call update attributes"){ controller.update_backer } end @@ -125,7 +141,10 @@ let(:moip_query_response) do {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso"} end - before{ backer.should_not_receive(:update_attributes) } + before do + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) + backer.should_not_receive(:update_attributes) + end it("should never call update attributes"){ controller.update_backer } end @@ -134,6 +153,7 @@ {"ID"=>"201210191926185570000024694351", "Status"=>"Sucesso", "Autorizacao"=>{"Pagador"=>{"Nome"=>"juliana.giopato@hotmail.com", "Email"=>"juliana.giopato@hotmail.com"}, "EnderecoCobranca"=>{"Logradouro"=>"Rua sócrates abraão ", "Numero"=>"16.0", "Complemento"=>"casa 19", "Bairro"=>"Campo Limpo", "CEP"=>"05782-470", "Cidade"=>"São Paulo", "Estado"=>"SP", "Pais"=>"BRA", "TelefoneFixo"=>"1184719963"}, "Recebedor"=>{"Nome"=>"Catarse", "Email"=>"financeiro@catarse.me"}, "Pagamento"=>[{"Data"=>"2012-10-17T13:06:07.000-03:00", "DataCredito"=>"2012-10-19T00:00:00.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"1.34", "ValorLiquido"=>"48.66", "FormaPagamento"=>"BoletoBancario", "InstituicaoPagamento"=>"Bradesco", "Status"=>"Autorizado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5258"}, {"Data"=>"2012-10-17T13:05:49.000-03:00", "TotalPago"=>"50.00", "TaxaParaPagador"=>"0.00", "TaxaMoIP"=>"3.09", "ValorLiquido"=>"46.91", "FormaPagamento"=>"CartaoDebito", "InstituicaoPagamento"=>"Visa", "Status"=>"Iniciado", "Parcela"=>{"TotalParcelas"=>"1"}, "CodigoMoIP"=>"0000.1325.5248"}]}} end before do + MoIP.should_receive(:query).with(backer.payment_token).and_return(moip_query_response) payment = moip_query_response["Autorizacao"]["Pagamento"].first backer.should_receive(:update_attributes).with({ payment_id: payment["CodigoMoIP"], From 388b7bc4a63a323cf60fa8c8940cd5d1e09198f4 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Thu, 6 Jun 2013 11:40:11 -0300 Subject: [PATCH 033/102] updated gems to use catarse fork of moip-ruby --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 1ef4286..a18bb56 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,6 @@ source "http://rubygems.org" gemspec -gem 'moip', git: 'git://github.com/moiplabs/moip-ruby.git' +gem 'moip', git: 'git://github.com/catarse/moip-ruby.git' gem 'enumerate_it' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index a3a1534..bc0bcc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,11 @@ GIT - remote: git://github.com/moiplabs/moip-ruby.git - revision: 2b61a77355cb8c5487ff68be0eef8873696a110e + remote: git://github.com/catarse/moip-ruby.git + revision: 39f9dab38fc33fe9bf614deb8e90ca166ab2d6bf specs: moip (1.0.2) activesupport (>= 2.3.2) httparty (~> 0.6.1) - nokogiri (~> 1.4.3) + nokogiri (~> 1.5.0) PATH remote: . @@ -57,7 +57,7 @@ GEM factory_girl_rails (4.2.1) factory_girl (~> 4.2.0) railties (>= 3.0.0) - hike (1.2.2) + hike (1.2.3) httparty (0.6.1) crack (= 0.1.8) i18n (0.6.1) @@ -68,8 +68,8 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.23) - multi_json (1.7.3) - nokogiri (1.4.7) + multi_json (1.7.6) + nokogiri (1.5.9) pg (0.15.1) polyglot (0.3.3) rack (1.4.5) @@ -101,7 +101,7 @@ GEM rspec-expectations (2.13.0) diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.13.1) - rspec-rails (2.13.1) + rspec-rails (2.13.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -115,7 +115,7 @@ GEM tilt (~> 1.1, != 1.3.0) thor (0.18.1) tilt (1.4.1) - treetop (1.4.12) + treetop (1.4.14) polyglot polyglot (>= 0.3.1) tzinfo (0.3.37) From 739a40ada7ffc29b64c0868ad7b3320c22dafa4c Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Thu, 8 Aug 2013 14:06:18 -0300 Subject: [PATCH 034/102] changed dependency to rails 3.2.12 --- Gemfile.lock | 2 +- catarse_moip.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bc0bcc4..1764a7d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,7 +12,7 @@ PATH specs: catarse_moip (0.1.1) libxml-ruby (~> 2.6.0) - rails (~> 3.2.13) + rails (~> 3.2.12) GEM remote: http://rubygems.org/ diff --git a/catarse_moip.gemspec b/catarse_moip.gemspec index a4c0715..f1ac04e 100644 --- a/catarse_moip.gemspec +++ b/catarse_moip.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split($\) s.test_files = s.files.grep(%r{^(test|spec|features)/}) - s.add_dependency "rails", "~> 3.2.13" + s.add_dependency "rails", "~> 3.2.12" s.add_dependency('libxml-ruby', '~> 2.6.0') s.add_development_dependency "rspec-rails" From 1f7f35fa7637aa3435d077ace5030cac73ef48a4 Mon Sep 17 00:00:00 2001 From: Diogo Biazus Date: Mon, 12 Aug 2013 18:23:18 -0300 Subject: [PATCH 035/102] now we can test the engine using jasmine-node, how cool is that eh? --- Rakefile | 9 +- .../javascripts/catarse_moip/moip_form.js | 10 +- spec/javascripts/moip_form_spec.js | 25 + spec/javascripts/support/app.js | 1 + spec/javascripts/support/backbone.js | 1571 +++ spec/javascripts/support/jquery.js | 8829 +++++++++++++++++ spec/javascripts/support/skull.js | 64 + spec/javascripts/support/underscore.js | 1227 +++ 8 files changed, 11729 insertions(+), 7 deletions(-) create mode 100644 spec/javascripts/moip_form_spec.js create mode 100644 spec/javascripts/support/app.js create mode 100644 spec/javascripts/support/backbone.js create mode 100644 spec/javascripts/support/jquery.js create mode 100644 spec/javascripts/support/skull.js create mode 100644 spec/javascripts/support/underscore.js diff --git a/Rakefile b/Rakefile index 37f9b2d..6631f5b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,7 @@ #!/usr/bin/env rake begin require 'bundler/setup' + require 'rspec/core/rake_task' rescue LoadError puts 'You must `gem install bundler` and `bundle install` to run rake tasks' end @@ -20,8 +21,12 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end +Bundler::GemHelper.install_tasks +RSpec::Core::RakeTask.new(:spec) +task :jasmine do + sh "jasmine-node spec/javascripts" +end - -Bundler::GemHelper.install_tasks +task default: [:spec, :jasmine] diff --git a/app/assets/javascripts/catarse_moip/moip_form.js b/app/assets/javascripts/catarse_moip/moip_form.js index b7cd6ac..bd9f321 100644 --- a/app/assets/javascripts/catarse_moip/moip_form.js +++ b/app/assets/javascripts/catarse_moip/moip_form.js @@ -1,4 +1,4 @@ -CATARSE.MoipForm = Backbone.View.extend({ +App.addChild('MoipForm', { el: 'form.moip', getMoipToken: function(onSuccess){ @@ -70,10 +70,10 @@ CATARSE.MoipForm = Backbone.View.extend({ this.loader = this.$('.loader'); - this.paymentChoice = new CATARSE.PaymentChoice(); - this.paymentCard = new CATARSE.PaymentCard({moipForm: this}); - this.paymentSlip = new CATARSE.PaymentSlip({moipForm: this}); - this.paymentAccount = new CATARSE.PaymentAccount({moipForm: this}); + //this.paymentChoice = new CATARSE.PaymentChoice(); + //this.paymentCard = new CATARSE.PaymentCard({moipForm: this}); + //this.paymentSlip = new CATARSE.PaymentSlip({moipForm: this}); + //this.paymentAccount = new CATARSE.PaymentAccount({moipForm: this}); window.checkoutSuccessful = _.bind(this.checkoutSuccessful, this); window.checkoutFailure = _.bind(this.checkoutFailure, this); } diff --git a/spec/javascripts/moip_form_spec.js b/spec/javascripts/moip_form_spec.js new file mode 100644 index 0000000..3a8eb2e --- /dev/null +++ b/spec/javascripts/moip_form_spec.js @@ -0,0 +1,25 @@ +global.window = require("jsdom") + .jsdom() + .createWindow(); +global.jQuery = global.$ = require("jquery"); + +global._ = global.underscore = require("./support/underscore"); +global.Backbone = require("./support/backbone"); +Backbone.$ = $; + +global.Skull = require('./support/skull'); +require('./support/app'); +require("../../app/assets/javascripts/catarse_moip/moip_form"); + +describe("MoipForm", function() { + var view; + + beforeEach(function() { + view = new App.views.MoipForm(); + }); + + it("should be true", function() { + expect(true).toEqual(true); + }); +}); + diff --git a/spec/javascripts/support/app.js b/spec/javascripts/support/app.js new file mode 100644 index 0000000..0ac5040 --- /dev/null +++ b/spec/javascripts/support/app.js @@ -0,0 +1 @@ +global.App = window.App = Skull.View.extend({}); diff --git a/spec/javascripts/support/backbone.js b/spec/javascripts/support/backbone.js new file mode 100644 index 0000000..3512d42 --- /dev/null +++ b/spec/javascripts/support/backbone.js @@ -0,0 +1,1571 @@ +// Backbone.js 1.0.0 + +// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.0.0'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeners = this._listeners; + if (!listeners) return this; + var deleteListener = !name && !callback; + if (typeof name === 'object') callback = this; + if (obj) (listeners = {})[obj._listenerId] = obj; + for (var id in listeners) { + listeners[id].off(name, callback, this); + if (deleteListener) delete this._listeners[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); + listeners[id] = obj; + if (typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + _.extend(this, _.pick(options, modelOptions)); + if (options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // A list of options to be attached directly to the model, if provided. + var modelOptions = ['url', 'urlRoot', 'collection']; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; + + options = _.extend({validate: true}, options); + + // Do not persist invalid models. + if (!this._validate(attrs, options)) return false; + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.url) this.url = options.url; + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, merge: false, remove: false}; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.defaults(options || {}, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults(options || {}, setOptions); + if (options.parse) models = this.parse(models, options); + if (!_.isArray(models)) models = models ? [models] : []; + var i, l, model, attrs, existing, sort; + var at = options.at; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(models[i], options))) continue; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(model)) { + if (options.remove) modelMap[existing.cid] = true; + if (options.merge) { + existing.set(model.attributes, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + + // This is a new model, push it to the `toAdd` list. + } else if (options.add) { + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + } + + // Remove nonexistent models if appropriate. + if (options.remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + splice.apply(this.models, [at, 0].concat(toAdd)); + } else { + push.apply(this.models, toAdd); + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + if (options.silent) return this; + + // Trigger `add` events. + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + + // Trigger `sort` if the collection was sorted. + if (sort) this.trigger('sort', this, options); + return this; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: this.length}, options)); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function(begin, end) { + return this.models.slice(begin, end); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id != null ? obj.id : obj.cid || obj]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Figure out the smallest index at which a model should be inserted so as + // to maintain order. + sortedIndex: function(model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(resp) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options || (options = {}); + options.collection = this; + var model = new this.model(attrs, options); + if (!model._validate(attrs, options)) { + this.trigger('invalid', this, attrs, options); + return false; + } + return model; + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', + 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be prefered to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save' + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(e.g. model, collection, id, className)* are + // attached directly to the view. See `viewOptions` for an exhaustive + // list. + _configure: function(options) { + if (this.options) options = _.extend({}, _.result(this, 'options'), options); + _.extend(this, _.pick(options, viewOptions)); + this.options = options; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && window.ActiveXObject && + !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('