From a4e19ddc985fb452f33fc6c620e269fa6563211b Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:12:16 +1100 Subject: [PATCH 01/30] Tweaking the change hub text --- .../darkswarm/controllers/account_sidebar_controller.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee index 247ef933e8..b7180fab49 100644 --- a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee @@ -11,6 +11,6 @@ window.AccountSidebarCtrl = Darkswarm.controller "AccountSidebarCtrl", ($scope, $scope.emptyCart = (href, ev)-> console.log href if $(ev.delegateTarget).hasClass "empties-cart" - location.href = href if confirm "Changing your collection date will clear your cart." + location.href = href if confirm "Changing your Hub will clear your cart." else location.href = href From 02fd21d99e1ece56898d016a0b7f9539d3dac2e0 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:29:35 +1100 Subject: [PATCH 02/30] gs --- app/views/layouts/_bugherd_script.html.haml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/layouts/_bugherd_script.html.haml b/app/views/layouts/_bugherd_script.html.haml index 326c48da00..ad6fe585f5 100644 --- a/app/views/layouts/_bugherd_script.html.haml +++ b/app/views/layouts/_bugherd_script.html.haml @@ -1,4 +1,4 @@ -- if Rails.env.staging? +- if Rails.env.staging? or Rails.env.production? :javascript (function (d, t) { var bh = d.createElement(t), s = d.getElementsByTagName(t)[0]; @@ -7,11 +7,12 @@ s.parentNode.insertBefore(bh, s); })(document, 'script'); -- elsif Rails.env.production? - :javascript - (function (d, t) { - var bh = d.createElement(t), s = d.getElementsByTagName(t)[0]; - bh.type = 'text/javascript'; - bh.src = '//www.bugherd.com/sidebarv2.js?apikey=xro3uv55objies58o2wrua'; - s.parentNode.insertBefore(bh, s); - })(document, 'script'); + +-#- elsif Rails.env.production? + -#:javascript + -#(function (d, t) { + -#var bh = d.createElement(t), s = d.getElementsByTagName(t)[0]; + -#bh.type = 'text/javascript'; + -#bh.src = '//www.bugherd.com/sidebarv2.js?apikey=xro3uv55objies58o2wrua'; + -#s.parentNode.insertBefore(bh, s); + -#})(document, 'script'); From f8f37abe324c39649d621d1e847cc0f7e36f9501 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:44:36 +1100 Subject: [PATCH 03/30] Reworking the checkout spec slightly --- .../features/consumer/shopping/checkout_spec.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 211023b5a6..71d4cd72ab 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -9,6 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do let(:supplier) { create(:supplier_enterprise) } let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { Spree::Order.last } before do create_enterprise_group_for distributor @@ -16,6 +17,7 @@ feature "As a consumer I want to check out my cart", js: true do exchange.variants << product.master end + # Disabled :in for performance reasons [:out].each do |auth_state| describe "logged #{auth_state.to_s}, distributor selected, order cycle selected, product in cart" do let(:user) { create_enterprise_user } @@ -153,15 +155,20 @@ end def select_distributor visit "/" click_link distributor.name + #@order = Spree::Order.last end +# This method is naughty and writes to the DB directly +# Because loading the whole Angular app is slow def select_order_cycle - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - visit "/shop" - select exchange.pickup_time, from: "order_cycle_id" + #exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) + #visit "/shop" + #select exchange.pickup_time, from: "order_cycle_id" + order.update_attribute :order_cycle, order_cycle end def add_product_to_cart - fill_in "variants[#{product.master.id}]", with: product.master.on_hand - 1 - first("form.custom > input.button.right").click + #fill_in "variants[#{product.master.id}]", with: product.master.on_hand - 1 + #first("form.custom > input.button.right").click + create(:line_item, variant: product.master, order: order) end From 5153c44aa0be39433a27ae546d26dfea67e1f1e9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:53:37 +1100 Subject: [PATCH 04/30] Refactoring checkout specs some more --- .rspec | 1 + .../consumer/shopping/checkout_auth_spec.rb | 2 ++ .../shopping/checkout_plumbing_spec.rb | 2 ++ .../consumer/shopping/checkout_spec.rb | 22 +------------------ spec/support/request/shop_workflow.rb | 21 ++++++++++++++++++ 5 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 spec/support/request/shop_workflow.rb diff --git a/.rspec b/.rspec index 53607ea52b..333fedd86c 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --colour +--profile diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index de14f121e7..7c5e4e8fe1 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' feature "As a consumer I want to check out my cart", js: true do include AuthenticationWorkflow include WebHelper + include ShopWorkflow let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { Spree::Order.last } before do create_enterprise_group_for distributor diff --git a/spec/features/consumer/shopping/checkout_plumbing_spec.rb b/spec/features/consumer/shopping/checkout_plumbing_spec.rb index b972667ca9..32d627c5b1 100644 --- a/spec/features/consumer/shopping/checkout_plumbing_spec.rb +++ b/spec/features/consumer/shopping/checkout_plumbing_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' feature "As a consumer I want to check out my cart", js: true do include AuthenticationWorkflow + include ShopWorkflow include WebHelper let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { Spree::Order.last } before do create_enterprise_group_for distributor diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 71d4cd72ab..c227e5ef19 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' feature "As a consumer I want to check out my cart", js: true do include AuthenticationWorkflow + include ShopWorkflow include WebHelper let(:distributor) { create(:distributor_enterprise) } @@ -151,24 +152,3 @@ feature "As a consumer I want to check out my cart", js: true do end end - -def select_distributor - visit "/" - click_link distributor.name - #@order = Spree::Order.last -end - -# This method is naughty and writes to the DB directly -# Because loading the whole Angular app is slow -def select_order_cycle - #exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - #visit "/shop" - #select exchange.pickup_time, from: "order_cycle_id" - order.update_attribute :order_cycle, order_cycle -end - -def add_product_to_cart - #fill_in "variants[#{product.master.id}]", with: product.master.on_hand - 1 - #first("form.custom > input.button.right").click - create(:line_item, variant: product.master, order: order) -end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb new file mode 100644 index 0000000000..98b50f5ddf --- /dev/null +++ b/spec/support/request/shop_workflow.rb @@ -0,0 +1,21 @@ +module ShopWorkflow + def select_distributor + visit "/" + click_link distributor.name + end + + # These methods are naughty and write to the DB directly + # Because loading the whole Angular app is slow + def select_order_cycle + #exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) + #visit "/shop" + #select exchange.pickup_time, from: "order_cycle_id" + order.update_attribute :order_cycle, order_cycle + end + + def add_product_to_cart + #fill_in "variants[#{product.master.id}]", with: product.master.on_hand - 1 + #first("form.custom > input.button.right").click + create(:line_item, variant: product.master, order: order) + end +end From da479b7534863ac9f7fc74d3953533f8ebbb0aa5 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:01:36 +1100 Subject: [PATCH 05/30] Fast version of OC selection --- spec/features/consumer/shopping/shopping_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 1b3f7b613c..6f7da7add0 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -6,6 +6,7 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "Viewing a distributor" do let(:distributor) { create(:distributor_enterprise) } + let(:order) { Spree::Order.last } before do #temporarily using the old way to select distributor create_enterprise_group_for distributor @@ -326,8 +327,9 @@ def build_and_select_order_cycle exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.update_attribute :pickup_time, "frogs" exchange.variants << product.master + order.update_attribute :order_cycle, oc + #select "frogs", :from => "order_cycle_id" visit shop_path - select "frogs", :from => "order_cycle_id" exchange end @@ -337,7 +339,8 @@ def build_and_select_order_cycle_with_variants exchange.update_attribute :pickup_time, "frogs" exchange.variants << product.master exchange.variants << variant + #select "frogs", :from => "order_cycle_id" + order.update_attribute :order_cycle, oc visit shop_path - select "frogs", :from => "order_cycle_id" exchange end From 749abd82e8f22f2ed0010645b7078e0e4a079763 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:02:25 +1100 Subject: [PATCH 06/30] Undoing previous change: no performance fix --- spec/features/consumer/shopping/shopping_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 6f7da7add0..1b3f7b613c 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -6,7 +6,6 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "Viewing a distributor" do let(:distributor) { create(:distributor_enterprise) } - let(:order) { Spree::Order.last } before do #temporarily using the old way to select distributor create_enterprise_group_for distributor @@ -327,9 +326,8 @@ def build_and_select_order_cycle exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.update_attribute :pickup_time, "frogs" exchange.variants << product.master - order.update_attribute :order_cycle, oc - #select "frogs", :from => "order_cycle_id" visit shop_path + select "frogs", :from => "order_cycle_id" exchange end @@ -339,8 +337,7 @@ def build_and_select_order_cycle_with_variants exchange.update_attribute :pickup_time, "frogs" exchange.variants << product.master exchange.variants << variant - #select "frogs", :from => "order_cycle_id" - order.update_attribute :order_cycle, oc visit shop_path + select "frogs", :from => "order_cycle_id" exchange end From bd4623bc71185d16bebc8ceb7c54b1f8aacb361a Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:11:48 +1100 Subject: [PATCH 07/30] Reworking the tests a little more for FASTER --- spec/features/consumer/shopping/checkout_spec.rb | 2 -- spec/features/consumer/shopping/shopping_spec.rb | 5 +---- spec/support/request/shop_workflow.rb | 8 +++----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index c227e5ef19..f65a8bc6aa 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -14,8 +14,6 @@ feature "As a consumer I want to check out my cart", js: true do before do create_enterprise_group_for distributor - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.variants << product.master end # Disabled :in for performance reasons diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 1b3f7b613c..b9defdcc1b 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -13,13 +13,10 @@ feature "As a consumer I want to shop with a distributor", js: true do click_link distributor.name end - it "shows a distributor" do + it "shows a distributor with images" do visit shop_path page.should have_text distributor.name - end - it "shows distributor images" do - visit shop_path find("#tab_about a").click first("distributor img")['src'].should == distributor.logo.url(:thumb) first("#about img")['src'].should == distributor.promo_image.url(:large) diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 98b50f5ddf..414c7a4fa1 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -1,5 +1,6 @@ module ShopWorkflow def select_distributor + # If no order cycles are available this is much faster visit "/" click_link distributor.name end @@ -7,15 +8,12 @@ module ShopWorkflow # These methods are naughty and write to the DB directly # Because loading the whole Angular app is slow def select_order_cycle - #exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - #visit "/shop" - #select exchange.pickup_time, from: "order_cycle_id" + exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange.variants << product.master order.update_attribute :order_cycle, order_cycle end def add_product_to_cart - #fill_in "variants[#{product.master.id}]", with: product.master.on_hand - 1 - #first("form.custom > input.button.right").click create(:line_item, variant: product.master, order: order) end end From d9e86ae395f24198ed17b91f9cd81d2980ddc58d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 16:58:50 +1100 Subject: [PATCH 08/30] Some more minor refactoring --- spec/features/consumer/shopping/checkout_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index f65a8bc6aa..00f8455853 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -37,12 +37,9 @@ feature "As a consumer I want to check out my cart", js: true do distributor.shipping_methods << sm2 visit "/shop/checkout" end - it "shows all shipping methods" do + it "shows all shipping methods, but doesn't show ship address when not needed" do page.should have_content "Frogs" page.should have_content "Donkeys" - end - - it "doesn't show ship address forms when a shipping method wants no address" do choose(sm2.name) find("#ship_address", visible: false).visible?.should be_false end From e2fb593baf2f3312dceff8ab2767bcf6773e12b3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 28 Mar 2014 17:25:52 +1100 Subject: [PATCH 09/30] Fix deployment issue: sass-rails needs to be in default group for application.rb config, and in assets group so that it's required. --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 4d8389bd2c..dec618a871 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' gem 'sass', "~> 3.2" -gem 'sass-rails', '~> 3.2.3' +gem 'sass-rails', '~> 3.2.3', groups: [:default, :assets] gem 'aws-sdk' gem 'db2fog' gem 'andand' From 9e74a7298543b75ff27baf1ee965e0108e54414d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 10:35:16 +1100 Subject: [PATCH 10/30] Fix silent fail when order cycle coordinator not filled out --- app/views/admin/order_cycles/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index e2c0dd3ea9..6eacb9f58f 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -31,7 +31,7 @@ %h2 Coordinator = f.label :coordinator_id, 'Coordinator' -= f.collection_select :coordinator_id, coordinating_enterprises, :id, :name, {}, {'ng-model' => 'order_cycle.coordinator_id', 'ofn-on-change' => 'order_cycle.coordinator_fees = []', 'required' => true} += f.collection_select :coordinator_id, coordinating_enterprises, :id, :name, {include_blank: true}, {'ng-model' => 'order_cycle.coordinator_id', 'ofn-on-change' => 'order_cycle.coordinator_fees = []', 'required' => true} = render 'coordinator_fees', f: f From 98d599e5f71d88f098a5f3332e8e2d17f2614b55 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 11:38:59 +1100 Subject: [PATCH 11/30] Fix order cycle failing to save after a failing submit to server --- .../admin/order_cycle.js.erb.coffee | 17 ++++- .../unit/order_cycle_spec.js.coffee | 65 +++++++++++++------ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 807f0184b7..5b018e6548 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -286,12 +286,27 @@ angular.module('order_cycle', ['ngResource']) console.log('Failed to update order cycle') dataForSubmit: -> - data = angular.extend({}, this.order_cycle) + data = this.deepCopy() data = this.removeInactiveExchanges(data) data = this.translateCoordinatorFees(data) data = this.translateExchangeFees(data) data + deepCopy: -> + data = angular.extend({}, this.order_cycle) + + # Copy exchanges + data.incoming_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.incoming_exchanges) if this.order_cycle.incoming_exchanges? + data.outgoing_exchanges = (angular.extend {}, exchange for exchange in this.order_cycle.outgoing_exchanges) if this.order_cycle.outgoing_exchanges? + + # Copy exchange fees + all_exchanges = (data.incoming_exchanges || []) + (data.outgoing_exchanges || []) + for exchange in all_exchanges + if exchange.enterprise_fees? + exchange.enterprise_fees = (angular.extend {}, fee for fee in exchange.enterprise_fees) + + data + removeInactiveExchanges: (order_cycle) -> order_cycle.incoming_exchanges = (exchange for exchange in order_cycle.incoming_exchanges when exchange.active) diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 5441d5c0b0..d6d0ac882b 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -770,35 +770,62 @@ describe 'OrderCycle services', -> ] it 'converts coordinator fees into a list of ids', -> - data = + order_cycle = coordinator_fees: [ {id: 1} {id: 2} ] - data = OrderCycle.translateCoordinatorFees(data) + data = OrderCycle.translateCoordinatorFees(order_cycle) expect(data.coordinator_fees).toBeUndefined() expect(data.coordinator_fee_ids).toEqual [1, 2] - it 'converts exchange fees into a list of ids', -> - data = - incoming_exchanges: [ - enterprise_fees: [ - {id: 1} - {id: 2} + it "preserves original data when converting coordinator fees", -> + OrderCycle.order_cycle = + coordinator_fees: [ + {id: 1} + {id: 2} ] - ] - outgoing_exchanges: [ - enterprise_fees: [ - {id: 3} - {id: 4} + + data = OrderCycle.deepCopy() + data = OrderCycle.translateCoordinatorFees(data) + + expect(OrderCycle.order_cycle.coordinator_fees).toEqual [{id: 1}, {id: 2}] + expect(OrderCycle.order_cycle.coordinator_fee_ids).toBeUndefined() + + describe "converting exchange fees into a list of ids", -> + order_cycle = null + data = null + + beforeEach -> + order_cycle = + incoming_exchanges: [ + enterprise_fees: [ + {id: 1} + {id: 2} + ] ] - ] + outgoing_exchanges: [ + enterprise_fees: [ + {id: 3} + {id: 4} + ] + ] + OrderCycle.order_cycle = order_cycle - data = OrderCycle.translateExchangeFees(data) + data = OrderCycle.deepCopy() + data = OrderCycle.translateExchangeFees(data) - expect(data.incoming_exchanges[0].enterprise_fees).toBeUndefined() - expect(data.outgoing_exchanges[0].enterprise_fees).toBeUndefined() - expect(data.incoming_exchanges[0].enterprise_fee_ids).toEqual [1, 2] - expect(data.outgoing_exchanges[0].enterprise_fee_ids).toEqual [3, 4] + it 'converts exchange fees into a list of ids', -> + expect(data.incoming_exchanges[0].enterprise_fees).toBeUndefined() + expect(data.outgoing_exchanges[0].enterprise_fees).toBeUndefined() + expect(data.incoming_exchanges[0].enterprise_fee_ids).toEqual [1, 2] + expect(data.outgoing_exchanges[0].enterprise_fee_ids).toEqual [3, 4] + + it "preserves original data when converting exchange fees", -> + expect(order_cycle.incoming_exchanges[0].enterprise_fees).toEqual [{id: 1}, {id: 2}] + expect(order_cycle.outgoing_exchanges[0].enterprise_fees).toEqual [{id: 3}, {id: 4}] + expect(order_cycle.incoming_exchanges[0].enterprise_fee_ids).toBeUndefined() + expect(order_cycle.outgoing_exchanges[0].enterprise_fee_ids).toBeUndefined() + \ No newline at end of file From 32ffd05ba0a5d816c77f2e654346ba7672798fea Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 11:54:28 +1100 Subject: [PATCH 12/30] Order cycle can have the same enterprise participating as supplier, coordinator and distributor --- app/models/exchange.rb | 2 +- spec/models/exchange_spec.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/exchange.rb b/app/models/exchange.rb index 243a311bcc..122586ba96 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -11,7 +11,7 @@ class Exchange < ActiveRecord::Base has_many :enterprise_fees, :through => :exchange_fees validates_presence_of :order_cycle, :sender, :receiver - validates_uniqueness_of :sender_id, :scope => [:order_cycle_id, :receiver_id] + validates_uniqueness_of :sender_id, :scope => [:order_cycle_id, :receiver_id, :incoming] accepts_nested_attributes_for :variants diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index 403fceba87..9614a4d357 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -13,13 +13,17 @@ describe Exchange do end end - it "should not be valid when sender and receiver pair are not unique for its order cycle" do + it "should not be valid when (sender, receiver, direction) set are not unique for its order cycle" do e1 = create(:exchange) e2 = build(:exchange, - :order_cycle => e1.order_cycle, :sender => e1.sender, :receiver => e1.receiver) + :order_cycle => e1.order_cycle, :sender => e1.sender, :receiver => e1.receiver, :incoming => e1.incoming) e2.should_not be_valid + e2.incoming = !e2.incoming + e2.should be_valid + e2.incoming = !e2.incoming + e2.receiver = create(:enterprise) e2.should be_valid From 718e295f3fc91870b0579e02dcec69f50e659329 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 17:13:43 +1100 Subject: [PATCH 13/30] Some more minor test fiddling --- .../consumer/shopping/checkout_auth_spec.rb | 2 - .../shopping/checkout_plumbing_spec.rb | 2 - .../consumer/shopping/shopping_spec.rb | 44 ++++++------------- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 7c5e4e8fe1..6ad1ab89d4 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -13,8 +13,6 @@ feature "As a consumer I want to check out my cart", js: true do before do create_enterprise_group_for distributor - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.variants << product.master end describe "Login behaviour" do diff --git a/spec/features/consumer/shopping/checkout_plumbing_spec.rb b/spec/features/consumer/shopping/checkout_plumbing_spec.rb index 32d627c5b1..2295b07146 100644 --- a/spec/features/consumer/shopping/checkout_plumbing_spec.rb +++ b/spec/features/consumer/shopping/checkout_plumbing_spec.rb @@ -14,8 +14,6 @@ feature "As a consumer I want to check out my cart", js: true do before do create_enterprise_group_for distributor - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.variants << product.master end describe "Attempting to access checkout without meeting the preconditions" do it "redirects to the homepage if no distributor is selected" do diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index b9defdcc1b..89aa78eb5c 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -40,32 +40,27 @@ feature "As a consumer I want to shop with a distributor", js: true do end describe "selecting an order cycle" do + let(:oc1) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 2.days.from_now)} + let(:oc2) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 3.days.from_now)} + let(:exchange1) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:exchange2) { Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) } it "selects an order cycle if only one is open" do # create order cycle - oc1 = create(:simple_order_cycle, distributors: [distributor]) - exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.update_attribute :pickup_time, "turtles" + exchange1.update_attribute :pickup_time, "turtles" visit shop_path page.should have_selector "option[selected]", text: 'turtles' end describe "with multiple order cycles" do - let(:oc1) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 2.days.from_now)} - let(:oc2) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 3.days.from_now)} before do - exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.update_attribute :pickup_time, "frogs" - exchange = Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.update_attribute :pickup_time, "turtles" + exchange1.update_attribute :pickup_time, "frogs" + exchange2.update_attribute :pickup_time, "turtles" visit shop_path end - it "shows a select with all order cycles" do + it "shows a select with all order cycles, but doesn't show the products by default" do page.should have_selector "option", text: 'frogs' page.should have_selector "option", text: 'turtles' - end - - it "doesn't show the table before an order cycle is selected" do page.should_not have_selector("input.button.right", visible: true) end @@ -77,33 +72,20 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "with products in our order cycle" do let(:product) { create(:simple_product) } before do - exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) - exchange.variants << product.master + exchange1.variants << product.master visit shop_path end - it "allows us to select an order cycle" do - select "frogs", :from => "order_cycle_id" + it "allows us to select an order cycle, thus showing products" do + page.should_not have_content product.name Spree::Order.last.order_cycle.should == nil + + select "frogs", :from => "order_cycle_id" page.should have_selector "products" page.should have_content "Orders close 2 days from now" Spree::Order.last.order_cycle.should == oc1 - end - - it "doesn't show products before an order cycle is selected" do - page.should_not have_content product.name - end - - it "shows products when an order cycle has been selected" do - select "frogs", :from => "order_cycle_id" page.should have_content product.name end - - it "updates the orders close note when order cycle is changed" do - oc1.stub(:orders_close_at).and_return 3.days.from_now - select "turtles", :from => "order_cycle_id" - page.should have_content "Orders close 3 days from now" - end end end From 44049da3cbaf109eb669647e14156ba3f3d0e910 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 4 Apr 2014 12:36:12 +1300 Subject: [PATCH 14/30] Text change --- app/views/shop/checkout/_form.html.haml | 4 ++-- spec/features/consumer/shopping/checkout_spec.rb | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index 0445e28eed..8ac9f67c90 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -24,12 +24,12 @@ .large-6.columns = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" - %fieldset + %fieldset#billing %legend Billing Address = f.fields_for :bill_address, @order.bill_address do |ba| .row .large-12.columns - = ba.text_field :address1, label: "Billing Address", + = ba.text_field :address1, "ng-model" => "order.bill_address.address1" .row .large-12.columns diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 00f8455853..d7507189b9 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -72,7 +72,9 @@ feature "As a consumer I want to check out my cart", js: true do it "copies billing address to hidden shipping address fields" do choose(sm1.name) check "Shipping address same as billing address?" - fill_in "Billing Address", with: "testy" + within "#billing" do + fill_in "Address", with: "testy" + end within "#ship_address_hidden" do find("#order_ship_address_attributes_address1", visible: false).value.should == "testy" end From 5c999fd9cffcb0816544d66131a6bf183f2f72ad Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 4 Apr 2014 12:36:33 +1300 Subject: [PATCH 15/30] Adding a signup confirmation email --- app/mailers/spree/user_mailer_decorator.rb | 8 ++++++++ app/models/spree/user_decorator.rb | 5 +++++ .../user_mailer/signup_confirmation.text.erb | 13 ++++++++++++ spec/mailers/user_mailer_spec.rb | 20 +++++++++++++++++++ spec/models/spree/user_spec.rb | 9 +++++++++ 5 files changed, 55 insertions(+) create mode 100644 app/mailers/spree/user_mailer_decorator.rb create mode 100644 app/views/spree/user_mailer/signup_confirmation.text.erb create mode 100644 spec/mailers/user_mailer_spec.rb create mode 100644 spec/models/spree/user_spec.rb diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb new file mode 100644 index 0000000000..ff8bdc4691 --- /dev/null +++ b/app/mailers/spree/user_mailer_decorator.rb @@ -0,0 +1,8 @@ +Spree::UserMailer.class_eval do + + def signup_confirmation(user) + @user = user + mail(:to => user.email, :from => from_address, + :subject => 'Welcome to ' + Spree::Config[:site_name]) + end +end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 5acaff924e..9dbc8226f5 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -6,6 +6,7 @@ Spree.user_class.class_eval do accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true attr_accessible :enterprise_ids, :enterprise_roles_attributes + after_create :send_signup_confirmation def build_enterprise_roles Enterprise.all.each do |enterprise| @@ -14,4 +15,8 @@ Spree.user_class.class_eval do end end end + + def send_signup_confirmation + Spree::UserMailer.signup_confirmation(self).deliver + end end diff --git a/app/views/spree/user_mailer/signup_confirmation.text.erb b/app/views/spree/user_mailer/signup_confirmation.text.erb new file mode 100644 index 0000000000..c6e57efc8a --- /dev/null +++ b/app/views/spree/user_mailer/signup_confirmation.text.erb @@ -0,0 +1,13 @@ +Hello, + +Welcome to Australia's Open Food Network! Your login email is <%= @user.email %> + +You can go online and start shopping through food hubs and local producers you like at vic.openfoodnetwork.org.au + +We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at hello@openfoodnetwork.org + +Thanks for getting on board and we look forward to introducing you to many more great farmers, food hubs and food! + +Cheers, +Kirsten Larsen and the OFN Team + diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb new file mode 100644 index 0000000000..97e0eb4430 --- /dev/null +++ b/spec/mailers/user_mailer_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Spree::UserMailer do + let(:user) { build(:user) } + + after do + ActionMailer::Base.deliveries.clear + end + + before do + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + end + + it "sends an email when given a user" do + Spree::UserMailer.signup_confirmation(user).deliver + ActionMailer::Base.deliveries.count.should == 1 + end +end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb new file mode 100644 index 0000000000..4e9ad94fac --- /dev/null +++ b/spec/models/spree/user_spec.rb @@ -0,0 +1,9 @@ +describe Spree.user_class do + context "#create" do + + it "should send a signup email" do + Spree::UserMailer.should_receive(:signup_confirmation).and_return(double(:deliver => true)) + create(:user) + end + end +end From 1c22a2c848e8f3fbbbe792b9fcc7247e200d0e05 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 13:14:32 +1000 Subject: [PATCH 16/30] Getting in notification to select order cycle --- .../darkswarm/controllers/order_cycle_controller.js.coffee | 7 ++++++- .../javascripts/darkswarm/services/order_cycle.js.coffee | 7 +++---- .../shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js | 5 +++-- app/views/shop/shop/show.html.haml | 7 ++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee index fc6413705a..3d8dc31fda 100644 --- a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee @@ -1,5 +1,10 @@ -Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle) -> +Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle, $tour) -> $scope.order_cycle = OrderCycle.order_cycle $scope.OrderCycle = OrderCycle $scope.changeOrderCycle = -> + $tour.end() OrderCycle.push_order_cycle() + + if !OrderCycle.selected() + $tour.start() + diff --git a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee index 58d4d11946..c7a4473b03 100644 --- a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee @@ -7,9 +7,8 @@ Darkswarm.factory 'OrderCycle', ($resource, Product, orderCycleData) -> Product.update() @orders_close_at: -> - if @order_cycle + if @selected() @order_cycle.orders_close_at - else - "" + @selected: -> - @order_cycle != null + @order_cycle != null and !$.isEmptyObject(@order_cycle) and @order_cycle.orders_close_at != undefined diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js index fb1217f780..d4fb9527d8 100644 --- a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js +++ b/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js @@ -2058,7 +2058,7 @@ angular.module( 'mm.foundation.tour', [ 'mm.foundation.position', 'mm.foundation }; }]) -.directive( 'stepText', [ '$position', '$tooltip', '$tour', '$window', function ( $position, $tooltip, $tour, $window ) { +.directive( 'step', [ '$position', '$tooltip', '$tour', '$window', function ( $position, $tooltip, $tour, $window ) { function isElementInViewport( element ) { var rect = element[0].getBoundingClientRect(); @@ -2092,6 +2092,7 @@ angular.module( 'mm.foundation.tour', [ 'mm.foundation.position', 'mm.foundation return $tooltip( 'step', 'step', show ); }]); + angular.module('mm.foundation.typeahead', ['mm.foundation.position', 'mm.foundation.bindHtml']) /** @@ -2602,7 +2603,7 @@ angular.module("template/tour/tour.html", []).run(["$templateCache", function($t "

\n" + "

\n" + " Next\n" + - " End\n" + + //" Close\n" + " ×\n" + " \n" + "\n" + diff --git a/app/views/shop/shop/show.html.haml b/app/views/shop/shop/show.html.haml index df054840da..62d25b9670 100644 --- a/app/views/shop/shop/show.html.haml +++ b/app/views/shop/shop/show.html.haml @@ -1,12 +1,13 @@ %shop.darkswarm - content_for :order_cycle_form do + %strong.avenir Ready for %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", - "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}"} + "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", + step: "Please select an order cycle", "step-index" => "1","step-placement" => "bottom", "step-append-to-body" => "true"} - %closing - -#%img{src: "/icon/goes/here"} + %closing{"ng-if" => "OrderCycle.selected()"} Orders close %strong {{ OrderCycle.orders_close_at() | date_in_words }} = render partial: "shop/details" From 902eefa8bbfb651bd7f9833ddd556c967596a70a Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:10:22 +1000 Subject: [PATCH 17/30] Patching one test and simplifying the modifications to order cycle tooltip --- .../controllers/order_cycle_controller.js.coffee | 15 ++++++++++----- .../javascripts/darkswarm/darkswarm.js.coffee | 4 +++- app/views/shop/shop/show.html.haml | 3 ++- spec/features/consumer/shopping/checkout_spec.rb | 4 ++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee index 3d8dc31fda..29e81ae861 100644 --- a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee @@ -1,10 +1,15 @@ -Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle, $tour) -> +Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle, $timeout) -> $scope.order_cycle = OrderCycle.order_cycle $scope.OrderCycle = OrderCycle + $scope.changeOrderCycle = -> - $tour.end() OrderCycle.push_order_cycle() + $timeout -> + $("#order_cycle_id").trigger("closeTrigger") - if !OrderCycle.selected() - $tour.start() - + # Timeout forces this to be evaluated after everything is loaded + # This is a hack. We should probably write our own "popover" directive + # That takes an expression instead of a trigger, and binds to that + $timeout => + if !$scope.OrderCycle.selected() + $("#order_cycle_id").trigger("openTrigger") diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index bffcc780e3..3d4f3b804e 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,6 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation']).config ($httpProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' $httpProvider.defaults.headers.common.Accept = "application/json, text/javascript, */*" + + $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ) diff --git a/app/views/shop/shop/show.html.haml b/app/views/shop/shop/show.html.haml index 62d25b9670..f8380c6c39 100644 --- a/app/views/shop/shop/show.html.haml +++ b/app/views/shop/shop/show.html.haml @@ -5,11 +5,12 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", - step: "Please select an order cycle", "step-index" => "1","step-placement" => "bottom", "step-append-to-body" => "true"} + "popover-placement" => "bottom", "popover" => "testy", "popover-trigger" => "openTrigger"} %closing{"ng-if" => "OrderCycle.selected()"} Orders close %strong {{ OrderCycle.orders_close_at() | date_in_words }} + = render partial: "shop/details" %products.row diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index d7507189b9..7cf28675e1 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -112,7 +112,7 @@ feature "As a consumer I want to check out my cart", js: true do within "#details" do fill_in "First Name", with: "Will" fill_in "Last Name", with: "Marshall" - fill_in "Billing Address", with: "123 Your Face" + fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" fill_in "Customer E-Mail", with: "test@test.com" @@ -130,7 +130,7 @@ feature "As a consumer I want to check out my cart", js: true do within "#details" do fill_in "First Name", with: "Will" fill_in "Last Name", with: "Marshall" - fill_in "Billing Address", with: "123 Your Face" + fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" fill_in "Customer E-Mail", with: "test@test.com" From 6ae51eabe3f2bdd8d9911c0785d3485e288ccb44 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:29:42 +1000 Subject: [PATCH 18/30] Adding a helpful comment --- app/assets/javascripts/darkswarm/darkswarm.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 3d4f3b804e..6be3a4a155 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -3,4 +3,5 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.fou $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' $httpProvider.defaults.headers.common.Accept = "application/json, text/javascript, */*" + # This allows us to trigger these two events on tooltips $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ) From 1e04a3b5f708c197977d6540cf573eb3d0276767 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:29:51 +1000 Subject: [PATCH 19/30] Changing the confirm email text a little bit --- app/views/spree/order_mailer/confirm_email.text.erb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/views/spree/order_mailer/confirm_email.text.erb b/app/views/spree/order_mailer/confirm_email.text.erb index e72fe48433..3ebeb233fd 100644 --- a/app/views/spree/order_mailer/confirm_email.text.erb +++ b/app/views/spree/order_mailer/confirm_email.text.erb @@ -23,6 +23,15 @@ Payment Details <%= @order.payments.first.andand.payment_method.andand.description.andand.html_safe %> <% end %> + +<%- if @order.shipping_method.require_ship_address %> +============================================================ +Shipping Details +============================================================ +Your order will be shipped to: +<%= @order.ship_address.to_s %> +<% else %> + ============================================================ Collection / Delivery Details ============================================================ @@ -34,9 +43,11 @@ Collection / Delivery Details <% else %> <%= @order.distributor.next_collection_at %> <% end %> +<% end %> Thanks for your support. + <%= @order.distributor.contact %>, <%= @order.distributor.name %> <%= @order.distributor.phone %> From 4447730505a7f26e981b89f98ea0378e81cf27b7 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:46:48 +1000 Subject: [PATCH 20/30] Removing nothing special text and patching change password form slightly more --- app/controllers/shop/checkout_controller.rb | 1 - app/views/user_passwords/edit.html.haml | 31 +++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb index 2e817150f5..be97bd604f 100644 --- a/app/controllers/shop/checkout_controller.rb +++ b/app/controllers/shop/checkout_controller.rb @@ -30,7 +30,6 @@ class Shop::CheckoutController < Spree::CheckoutController if @order.state == "complete" || @order.completed? flash.notice = t(:order_processed_successfully) - flash[:commerce_tracking] = "nothing special" respond_with(@order, :location => order_path(@order)) else clear_ship_address diff --git a/app/views/user_passwords/edit.html.haml b/app/views/user_passwords/edit.html.haml index 0a27217f3e..3042161881 100644 --- a/app/views/user_passwords/edit.html.haml +++ b/app/views/user_passwords/edit.html.haml @@ -1,15 +1,16 @@ -.row - = f_form_for @spree_user, :as => :spree_user, :url => spree.spree_user_password_path, :method => :put do |f| - = render :partial => 'spree/shared/error_messages', :locals => { :target => @spree_user } - %fieldset - %legend= t(:change_my_password) - .row - .large-12.columns - = f.password_field :password - .row - .large-12.columns - = f.password_field :password_confirmation - = f.hidden_field :reset_password_token - .row - .large-12.columns - = f.submit t(:update_password), :class => 'button primary' += f_form_for @spree_user, :as => :spree_user, :url => spree.spree_user_password_path, :method => :put do |f| + = render :partial => 'spree/shared/error_messages', :locals => { :target => @spree_user } + %fieldset + .row + .small-12.medium-6.large-4.columns.medium-centered.large-centered + %legend= t(:change_my_password) + .row + .small-12.medium-6.large-4.columns.medium-centered.large-centered + = f.password_field :password + .row + .small-12.medium-6.large-4.columns.medium-centered.large-centered + = f.password_field :password_confirmation + = f.hidden_field :reset_password_token + .row + .small-10.medium-6.large-4.columns.small-centered + = f.submit t(:update_password), :class => 'button primary' From 577c91aca5500d67ffe34e51ada03bbf333cf29c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 16:15:19 +1000 Subject: [PATCH 21/30] Starting on the improved accordion checkout --- .../controllers/checkout_controller.js.coffee | 3 + .../stylesheets/darkswarm/forms.css.sass | 20 ++- app/views/shop/checkout/_billing.html.haml | 34 +++++ app/views/shop/checkout/_details.html.haml | 21 +++ app/views/shop/checkout/_form.html.haml | 136 +----------------- app/views/shop/checkout/_order.rabl | 11 +- app/views/shop/checkout/_payment.html.haml | 19 +++ app/views/shop/checkout/_shipping.html.haml | 81 +++++++++++ app/views/shop/checkout/edit.html.haml | 18 +-- .../consumer/shopping/checkout_auth_spec.rb | 69 +++------ .../consumer/shopping/checkout_spec.rb | 5 + spec/support/request/shop_workflow.rb | 4 + 12 files changed, 229 insertions(+), 192 deletions(-) create mode 100644 app/views/shop/checkout/_billing.html.haml create mode 100644 app/views/shop/checkout/_details.html.haml create mode 100644 app/views/shop/checkout/_payment.html.haml create mode 100644 app/views/shop/checkout/_shipping.html.haml diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 1d0604d76c..528e809b6f 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -18,6 +18,9 @@ Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, order) -> $scope.shippingMethod = -> $scope.order.shipping_methods[$scope.order.shipping_method_id] + $scope.paymentMethod = -> + $scope.order.payment_methods[$scope.order.payment_method_id] + $scope.shippingMethodChanged = -> $scope.require_ship_address = $scope.shippingMethod().require_ship_address if $scope.shippingMethod() diff --git a/app/assets/stylesheets/darkswarm/forms.css.sass b/app/assets/stylesheets/darkswarm/forms.css.sass index bada1fb68d..e519b2818f 100644 --- a/app/assets/stylesheets/darkswarm/forms.css.sass +++ b/app/assets/stylesheets/darkswarm/forms.css.sass @@ -1,6 +1,6 @@ @import variables -form +.darkswarm fieldset padding: 0px border: none @@ -12,5 +12,23 @@ form display: block width: 100% margin-bottom: 1em + color: #999999 + text-transform: uppercase + dd a + border: 1px solid $dark-grey + border-left: 0px + border-right: 0px + padding: 16px 24px + display: block + width: 100% + margin-bottom: 1em + color: #999999 text-transform: uppercase color: #999999 + font-weight: bold + text-transform: uppercase + background: transparent + .text-right + font-weight: normal + text-transform: none + diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml new file mode 100644 index 0000000000..ccf1d49091 --- /dev/null +++ b/app/views/shop/checkout/_billing.html.haml @@ -0,0 +1,34 @@ +%fieldset#billing + %accordion-group + %accordion-heading + .row + .large-6.columns + Billing + .large-6.columns.text-right + {{ order.bill_address.address1 }} + {{ order.bill_address.city }} + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .large-12.columns + = ba.text_field :address1, + "ng-model" => "order.bill_address.address1" + .row + .large-12.columns + = ba.text_field :address2, + "ng-model" => "order.bill_address.address2" + .row + .large-6.columns + + = ba.text_field :city, + "ng-model" => "order.bill_address.city" + + .large-6.columns + = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, + "ng-model" => "order.bill_address.state_id" + .row + .large-6.columns + = ba.text_field :zipcode, label: "Postcode", + "ng-model" => "order.bill_address.zipcode" + .large-6.columns.right + = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, + {include_blank: false}, "ng-model" => "order.bill_address.country_id" diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml new file mode 100644 index 0000000000..241a65c4d6 --- /dev/null +++ b/app/views/shop/checkout/_details.html.haml @@ -0,0 +1,21 @@ +%fieldset#details + %accordion-group + %accordion-heading + .row + .large-6.columns + Customer Details + .large-6.columns.text-right + {{ order.bill_address.firstname }} + {{ order.bill_address.lastname }} + .row + .large-6.columns + = f.text_field :email + = f.fields_for :bill_address, @order.bill_address do |ba| + .large-6.columns + = ba.text_field :phone, "ng-model" => "order.bill_address.phone" + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .large-6.columns + = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" + .large-6.columns + = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index 8ac9f67c90..759baabd64 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -9,135 +9,7 @@ -#{{ order | json }} .large-12.columns - %fieldset#details - %legend Customer Details - .row - .large-6.columns - = f.text_field :email - = f.fields_for :bill_address, @order.bill_address do |ba| - .large-6.columns - = ba.text_field :phone, "ng-model" => "order.bill_address.phone" - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-6.columns - = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" - .large-6.columns - = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" - - %fieldset#billing - %legend Billing Address - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-12.columns - = ba.text_field :address1, - "ng-model" => "order.bill_address.address1" - .row - .large-12.columns - = ba.text_field :address2, - "ng-model" => "order.bill_address.address2" - .row - .large-6.columns - - = ba.text_field :city, - "ng-model" => "order.bill_address.city" - - .large-6.columns - = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, - "ng-model" => "order.bill_address.state_id" - .row - .large-6.columns - = ba.text_field :zipcode, label: "Postcode", - "ng-model" => "order.bill_address.zipcode" - .large-6.columns.right - = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false}, "ng-model" => "order.bill_address.country_id" - - %fieldset#shipping - %legend Shipping - - for ship_method, i in current_distributor.shipping_methods.uniq - .row - .large-12.columns - -#= f.radio_button :shipping_method_id, ship_method.id, - -#text: ship_method.name, - -#"ng-change" => "shippingMethodChanged()", - -#"ng-model" => "order.shipping_method_id" - %label - = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-change" => "shippingMethodChanged()", - "ng-model" => "order.shipping_method_id" - = ship_method.name - - #distributor_address.panel{"ng-show" => "!require_ship_address"} - = @order.distributor.distributor_info.andand.html_safe - = @order.order_cycle.pickup_time_for(@order.distributor) - = @order.order_cycle.pickup_instructions_for(@order.distributor) - - = f.fields_for :ship_address, @order.ship_address do |sa| - - #ship_address{"ng-show" => "require_ship_address"} - %label - = hidden_field_tag "order[ship_address_same_as_billing]", "false" - = check_box_tag "order[ship_address_same_as_billing]", true, @order.ship_address_same_as_billing, - "ng-model" => "order.ship_address_same_as_billing" - Shipping address same as billing address? - - %div.visible{"ng-show" => "!order.ship_address_same_as_billing"} - .row - .large-12.columns - = sa.text_field :address1 - .row - - .large-12.columns - = sa.text_field :address2 - - .row - .large-6.columns - = sa.text_field :city - .large-6.columns - = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} - .row - .large-6.columns - = sa.text_field :zipcode, label: "Postcode" - .large-6.columns.right - = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false} - .row - .large-6.columns - = sa.text_field :firstname - .large-6.columns - = sa.text_field :lastname - .row - .large-6.columns - = sa.text_field :phone - - #ship_address_hidden{"ng-show" => "order.ship_address_same_as_billing"} - = sa.hidden_field :address1, "ng-value" => "order.bill_address.address1", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :address2, "ng-value" => "order.bill_address.address2", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :city, "ng-value" => "order.bill_address.city", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :country_id, "ng-value" => "order.bill_address.country_id", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :zipcode, "ng-value" => "order.bill_address.zipcode", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :firstname, "ng-value" => "order.bill_address.firstname", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :lastname, "ng-value" => "order.bill_address.lastname", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :phone, "ng-value" => "order.bill_address.phone", - "ng-disabled" => "!order.ship_address_same_as_billing" - - %fieldset#payment - %legend Payment Details - - current_order.available_payment_methods.each do |method| - .row - .large-12.columns - %label - = radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false, - "ng-model" => "order.payment_method_id" - = method.name - .row{"ng-show" => "order.payment_method_id == #{method.id}"} - .large-12.columns - = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } - + = render partial: "shop/checkout/details", locals: {f: f} + = render partial: "shop/checkout/billing", locals: {f: f} + = render partial: "shop/checkout/shipping", locals: {f: f} + = render partial: "shop/checkout/payment", locals: {f: f} diff --git a/app/views/shop/checkout/_order.rabl b/app/views/shop/checkout/_order.rabl index 09451830fb..4bba881fec 100644 --- a/app/views/shop/checkout/_order.rabl +++ b/app/views/shop/checkout/_order.rabl @@ -22,7 +22,16 @@ node :shipping_methods do Hash[current_order.distributor.shipping_methods.collect { |method| [method.id, { require_ship_address: method.require_ship_address, - price: method.compute_amount(current_order).to_f + price: method.compute_amount(current_order).to_f, + name: method.name + }] + }] +end + +node :payment_methods do + Hash[current_order.available_payment_methods.collect { + |method| [method.id, { + name: method.name }] }] end diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml new file mode 100644 index 0000000000..3a68e74321 --- /dev/null +++ b/app/views/shop/checkout/_payment.html.haml @@ -0,0 +1,19 @@ +%fieldset#payment + %accordion-group + %accordion-heading + .row + .large-6.columns + Payment Details + .large-6.columns.text-right + {{ paymentMethod().name }} + - current_order.available_payment_methods.each do |method| + .row + .large-12.columns + %label + = radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false, + "ng-model" => "order.payment_method_id" + = method.name + .row{"ng-show" => "order.payment_method_id == #{method.id}"} + .large-12.columns + = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } + diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml new file mode 100644 index 0000000000..2f57bb9cb5 --- /dev/null +++ b/app/views/shop/checkout/_shipping.html.haml @@ -0,0 +1,81 @@ +%fieldset#shipping + %accordion-group + %accordion-heading + .row + .large-6.columns + Shipping + .large-6.columns.text-right + {{ shippingMethod().name }} + - for ship_method, i in current_distributor.shipping_methods.uniq + .row + .large-12.columns + -#= f.radio_button :shipping_method_id, ship_method.id, + -#text: ship_method.name, + -#"ng-change" => "shippingMethodChanged()", + -#"ng-model" => "order.shipping_method_id" + %label + = radio_button_tag "order[shipping_method_id]", ship_method.id, false, + "ng-change" => "shippingMethodChanged()", + "ng-model" => "order.shipping_method_id" + = ship_method.name + + #distributor_address.panel{"ng-show" => "!require_ship_address"} + = @order.distributor.distributor_info.andand.html_safe + = @order.order_cycle.pickup_time_for(@order.distributor) + = @order.order_cycle.pickup_instructions_for(@order.distributor) + + = f.fields_for :ship_address, @order.ship_address do |sa| + + #ship_address{"ng-show" => "require_ship_address"} + %label + = hidden_field_tag "order[ship_address_same_as_billing]", "false" + = check_box_tag "order[ship_address_same_as_billing]", true, @order.ship_address_same_as_billing, + "ng-model" => "order.ship_address_same_as_billing" + Shipping address same as billing address? + + %div.visible{"ng-show" => "!order.ship_address_same_as_billing"} + .row + .large-12.columns + = sa.text_field :address1 + .row + + .large-12.columns + = sa.text_field :address2 + + .row + .large-6.columns + = sa.text_field :city + .large-6.columns + = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} + .row + .large-6.columns + = sa.text_field :zipcode, label: "Postcode" + .large-6.columns.right + = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, + {include_blank: false} + .row + .large-6.columns + = sa.text_field :firstname + .large-6.columns + = sa.text_field :lastname + .row + .large-6.columns + = sa.text_field :phone + + #ship_address_hidden{"ng-show" => "order.ship_address_same_as_billing"} + = sa.hidden_field :address1, "ng-value" => "order.bill_address.address1", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :address2, "ng-value" => "order.bill_address.address2", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :city, "ng-value" => "order.bill_address.city", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :country_id, "ng-value" => "order.bill_address.country_id", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :zipcode, "ng-value" => "order.bill_address.zipcode", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :firstname, "ng-value" => "order.bill_address.firstname", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :lastname, "ng-value" => "order.bill_address.lastname", + "ng-disabled" => "!order.ship_address_same_as_billing" + = sa.hidden_field :phone, "ng-value" => "order.bill_address.phone", + "ng-disabled" => "!order.ship_address_same_as_billing" diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 98cead9b9f..bca55e2a54 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -7,16 +7,18 @@ = render partial: "shop/details" %checkout - .row + %accordion.row{"close-others" => "false"} .large-9.columns - unless spree_current_user - .row - %section#checkout_login - .large-6.columns - = render partial: "shop/checkout/login" - %section#checkout_signup - .large-6.columns - = render partial: "shop/checkout/signup" + %fieldset + %accordion-group{heading: "User"} + .row + %section#checkout_login + .large-6.columns + = render partial: "shop/checkout/login" + %section#checkout_signup + .large-6.columns + = render partial: "shop/checkout/signup" .row = render partial: "shop/checkout/form" diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 6ad1ab89d4..f7c48664f8 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -23,61 +23,30 @@ feature "As a consumer I want to check out my cart", js: true do add_product_to_cart end - it "renders the login form if user is logged out" do - visit "/shop/checkout" - within "section[role='main']" do - page.should have_content "I HAVE AN OFN ACCOUNT" + + context "logged in" do + before do + login_to_consumer_section + visit "/shop/checkout" + end + it "does not not render the login form" do + within "section[role='main']" do + page.should_not have_content "USER" + end end end - it "does not not render the login form if user is logged in" do - login_to_consumer_section - visit "/shop/checkout" - within "section[role='main']" do - page.should_not have_content "I HAVE AN OFN ACCOUNT" - end - end - - it "renders the signup link if user is logged out" do - visit "/shop/checkout" - within "section[role='main']" do - page.should have_content "NEW TO OFN" - end - end - - it "does not not render the signup form if user is logged in" do - login_to_consumer_section - visit "/shop/checkout" - within "section[role='main']" do - page.should_not have_content "NEW TO OFN" - end - end - - it "redirects to the checkout page when logging in from the checkout page" do - visit "/shop/checkout" - within "#checkout_login" do - fill_in "spree_user[email]", with: user.email - fill_in "spree_user[password]", with: user.password - click_button "Login" + context "logged out" do + before do + visit "/shop/checkout" + save_and_open_page + toggle_accordion "User" end - current_path.should == "/shop/checkout" - within "section[role='main']" do - page.should_not have_content "I have an OFN Account" - end - end - - it "redirects to the checkout page when signing up from the checkout page" do - visit "/shop/checkout" - within "#checkout_signup" do - fill_in "spree_user[email]", with: "test@gmail.com" - fill_in "spree_user[password]", with: "password" - fill_in "spree_user[password_confirmation]", with: "password" - click_button "Sign Up" - end - current_path.should == "/shop/checkout" - within "section[role='main']" do - page.should_not have_content "Sign Up" + it "renders the login form if user is logged out" do + within "section[role='main']" do + page.should have_content "USER" + end end end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 7cf28675e1..47a6e97962 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -36,6 +36,11 @@ feature "As a consumer I want to check out my cart", js: true do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 visit "/shop/checkout" + click_link "USER" + click_link "CUSTOMER DETAILS" + click_link "BILLING" + click_link "SHIPPING" + click_link "PAYMENT DETAILS" end it "shows all shipping methods, but doesn't show ship address when not needed" do page.should have_content "Frogs" diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 414c7a4fa1..3a13754299 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -16,4 +16,8 @@ module ShopWorkflow def add_product_to_cart create(:line_item, variant: product.master, order: order) end + + def toggle_accordion(name) + find("dd[heading='#{name}'] > a").click + end end From fbcf06f5f5d189585a6c58dbde0a6d902a3d095d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 18:23:37 +1000 Subject: [PATCH 22/30] Adding some SUPER clever magic and fixing some regression issues --- .../controllers/checkout_controller.js.coffee | 28 +---- .../darkswarm/services/order.js.coffee | 32 +++++ app/views/shop/checkout/_payment.html.haml | 2 +- app/views/shop/checkout/_shipping.html.haml | 8 +- app/views/shop/checkout/_summary.html.haml | 5 +- .../consumer/shopping/checkout_spec.rb | 113 ++++++++++-------- spec/support/request/shop_workflow.rb | 2 +- 7 files changed, 109 insertions(+), 81 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/services/order.js.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 528e809b6f..f1cd687fa8 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,32 +1,10 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, order) -> +Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, Order) -> $scope.require_ship_address = false - $scope.order = order - - $scope.initialize = -> - # Our shipping_methods comes through as a hash like so: {id: requires_shipping_address} - # Here we default to the first shipping method if none is selected - $scope.order.shipping_method_id ||= Object.keys(order.shipping_methods)[0] - $scope.order.ship_address_same_as_billing = true if $scope.order.ship_address_same_as_billing == null - $scope.shippingMethodChanged() - - $scope.shippingPrice = -> - $scope.shippingMethod().price - - $scope.cartTotal = -> - $scope.shippingPrice() + $scope.order.display_total - - $scope.shippingMethod = -> - $scope.order.shipping_methods[$scope.order.shipping_method_id] - - $scope.paymentMethod = -> - $scope.order.payment_methods[$scope.order.payment_method_id] + $scope.order = $scope.Order = Order $scope.shippingMethodChanged = -> - $scope.require_ship_address = $scope.shippingMethod().require_ship_address if $scope.shippingMethod() + Order.shippingMethodChanged() $scope.purchase = (event)-> event.preventDefault() checkout.submit() - - $scope.initialize() - diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee new file mode 100644 index 0000000000..98d8b8fa42 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -0,0 +1,32 @@ +Darkswarm.factory 'Order', ($resource, Product, order)-> + + ## I am being clever here + ## order is a JSON object generated in shop/checkout/order.rabl + ## We're extending this to add methods while retaining the data! + + new class Order + constructor: -> + @[name] = method for name, method of order # Clone all data from the order JSON object + + # Our shipping_methods comes through as a hash like so: {id: requires_shipping_address} + # Here we default to the first shipping method if none is selected + @shipping_method_id ||= Object.keys(@shipping_methods)[0] + @ship_address_same_as_billing = true if @ship_address_same_as_billing == null + @shippingMethodChanged() + + shippingMethod: -> + @shipping_methods[@shipping_method_id] + + shippingMethodChanged: => + @require_ship_address = @shippingMethod().require_ship_address if @shippingMethod() + + shippingPrice: -> + @shippingMethod().price + + paymentMethod: -> + @payment_methods[@payment_method_id] + + + cartTotal: -> + @shippingPrice() + @display_total + diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml index 3a68e74321..e0efa6a165 100644 --- a/app/views/shop/checkout/_payment.html.haml +++ b/app/views/shop/checkout/_payment.html.haml @@ -5,7 +5,7 @@ .large-6.columns Payment Details .large-6.columns.text-right - {{ paymentMethod().name }} + {{ Order.paymentMethod().name }} - current_order.available_payment_methods.each do |method| .row .large-12.columns diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 2f57bb9cb5..5a93ca6075 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -5,7 +5,7 @@ .large-6.columns Shipping .large-6.columns.text-right - {{ shippingMethod().name }} + {{ Order.shippingMethod().name }} - for ship_method, i in current_distributor.shipping_methods.uniq .row .large-12.columns @@ -15,18 +15,18 @@ -#"ng-model" => "order.shipping_method_id" %label = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-change" => "shippingMethodChanged()", + "ng-change" => "order.shippingMethodChanged()", "ng-model" => "order.shipping_method_id" = ship_method.name - #distributor_address.panel{"ng-show" => "!require_ship_address"} + #distributor_address.panel{"ng-show" => "!order.require_ship_address"} = @order.distributor.distributor_info.andand.html_safe = @order.order_cycle.pickup_time_for(@order.distributor) = @order.order_cycle.pickup_instructions_for(@order.distributor) = f.fields_for :ship_address, @order.ship_address do |sa| - #ship_address{"ng-show" => "require_ship_address"} + #ship_address{"ng-show" => "order.require_ship_address"} %label = hidden_field_tag "order[ship_address_same_as_billing]", "false" = check_box_tag "order[ship_address_same_as_billing]", true, @order.ship_address_same_as_billing, diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 8acd254d69..a852e666e1 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -13,10 +13,10 @@ %td= adjustment.display_amount.to_html %tr %th Shipping - %td {{ shippingPrice() | currency }} + %td {{ Order.shippingPrice() | currency }} %tr %th Cart total - %td {{ cartTotal() | currency }} + %td {{ Order.cartTotal() | currency }} - if current_order.price_adjustment_totals.present? - current_order.price_adjustment_totals.each do |label, total| %tr @@ -25,3 +25,4 @@ = f.submit "Purchase", class: "button" %a.button.secondary{href: cart_url} Back to Cart + diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 47a6e97962..8343952170 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -35,53 +35,60 @@ feature "As a consumer I want to check out my cart", js: true do before do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 - visit "/shop/checkout" - click_link "USER" - click_link "CUSTOMER DETAILS" - click_link "BILLING" - click_link "SHIPPING" - click_link "PAYMENT DETAILS" - end - it "shows all shipping methods, but doesn't show ship address when not needed" do - page.should have_content "Frogs" - page.should have_content "Donkeys" - choose(sm2.name) - find("#ship_address", visible: false).visible?.should be_false end - context "When shipping method requires an address" do + context "on the checkout page" do before do + visit "/shop/checkout" + toggle_accordion "User" + toggle_accordion "Customer Details" + toggle_accordion "Billing" + toggle_accordion "Shipping" + toggle_accordion "Payment Details" + end + it "shows all shipping methods, but doesn't show ship address when not needed" do + page.should have_content "Frogs" + page.should have_content "Donkeys" + choose(sm2.name) + find("#ship_address", visible: false).visible?.should be_false + end + + context "When shipping method requires an address" do + before do + choose(sm1.name) + end + it "shows the hidden ship address fields by default" do + check "Shipping address same as billing address?" + find("#ship_address_hidden").visible?.should be_true + find("#ship_address > div.visible", visible: false).visible?.should be_false + + # Check it keeps state + click_button "Purchase" + toggle_accordion "Shipping" + find_field("Shipping address same as billing address?").should be_checked + end + + it "shows ship address forms when 'same as billing address' is unchecked" do + uncheck "Shipping address same as billing address?" + find("#ship_address_hidden", visible: false).visible?.should be_false + find("#ship_address > div.visible").visible?.should be_true + + # Check it keeps state + click_button "Purchase" + toggle_accordion "Shipping" + find_field("Shipping address same as billing address?").should_not be_checked + end + end + + it "copies billing address to hidden shipping address fields" do choose(sm1.name) - end - it "shows the hidden ship address fields by default" do check "Shipping address same as billing address?" - find("#ship_address_hidden").visible?.should be_true - find("#ship_address > div.visible", visible: false).visible?.should be_false - - # Check it keeps state - click_button "Purchase" - find_field("Shipping address same as billing address?").should be_checked - end - - it "shows ship address forms when 'same as billing address' is unchecked" do - uncheck "Shipping address same as billing address?" - find("#ship_address_hidden", visible: false).visible?.should be_false - find("#ship_address > div.visible").visible?.should be_true - - # Check it keeps state - click_button "Purchase" - find_field("Shipping address same as billing address?").should_not be_checked - end - end - - it "copies billing address to hidden shipping address fields" do - choose(sm1.name) - check "Shipping address same as billing address?" - within "#billing" do - fill_in "Address", with: "testy" - end - within "#ship_address_hidden" do - find("#order_ship_address_attributes_address1", visible: false).value.should == "testy" + within "#billing" do + fill_in "Address", with: "testy" + end + within "#ship_address_hidden" do + find("#order_ship_address_attributes_address1", visible: false).value.should == "testy" + end end end @@ -94,6 +101,11 @@ feature "As a consumer I want to check out my cart", js: true do pm1 # Lazy evaluation of ze create()s pm2 visit "/shop/checkout" + toggle_accordion "User" + toggle_accordion "Customer Details" + toggle_accordion "Billing" + toggle_accordion "Shipping" + toggle_accordion "Payment Details" end it "shows all available payment methods" do @@ -106,6 +118,7 @@ feature "As a consumer I want to check out my cart", js: true do choose sm2.name click_button "Purchase" current_path.should == "/shop/checkout" + toggle_accordion "Customer Details" page.should have_content "can't be blank" end @@ -117,11 +130,13 @@ feature "As a consumer I want to check out my cart", js: true do within "#details" do fill_in "First Name", with: "Will" fill_in "Last Name", with: "Marshall" + fill_in "Customer E-Mail", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + within "#billing" do fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" - fill_in "Customer E-Mail", with: "test@test.com" - fill_in "Phone", with: "0468363090" fill_in "City", with: "Melbourne" fill_in "Postcode", with: "3066" end @@ -135,13 +150,15 @@ feature "As a consumer I want to check out my cart", js: true do within "#details" do fill_in "First Name", with: "Will" fill_in "Last Name", with: "Marshall" + fill_in "Customer E-Mail", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + within "#billing" do + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" - fill_in "Customer E-Mail", with: "test@test.com" - fill_in "Phone", with: "0468363090" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" end check "Shipping address same as billing address?" click_button "Purchase" diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 3a13754299..1473afe477 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -18,6 +18,6 @@ module ShopWorkflow end def toggle_accordion(name) - find("dd[heading='#{name}'] > a").click + find("dd a", text: name.upcase).click end end From f6c173d0de3666682e91cf306e19ca33eed37545 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 12:58:37 +1000 Subject: [PATCH 23/30] Basics in place, about to rework karma config --- .../javascripts/darkswarm/all.js.coffee | 2 + .../controllers/checkout_controller.js.coffee | 11 +- .../javascripts/darkswarm/darkswarm.js.coffee | 2 +- .../darkswarm/services/order.js.coffee | 16 +- .../darkswarm/services/order_cycle.js.coffee | 7 +- .../shared/angular-local-storage.js | 170 ++++++++++++++++++ .../shop/checkout/_authentication.html.haml | 9 + app/views/shop/checkout/_details.html.haml | 2 +- app/views/shop/checkout/_order.rabl | 1 - app/views/shop/checkout/_shipping.html.haml | 11 +- app/views/shop/checkout/edit.html.haml | 10 +- app/views/shop/shop/_order_cycles.html.haml | 2 + app/views/shop/shop/show.html.haml | 2 +- .../consumer/shopping/checkout_auth_spec.rb | 2 - .../checkout_controller_spec.js.coffee | 27 +-- .../{ => services}/order_cycle_spec.js.coffee | 2 +- .../darkswarm/services/order_spec.js.coffee | 47 +++++ 17 files changed, 254 insertions(+), 69 deletions(-) create mode 100644 app/assets/javascripts/shared/angular-local-storage.js create mode 100644 app/views/shop/checkout/_authentication.html.haml rename spec/javascripts/unit/darkswarm/{ => services}/order_cycle_spec.js.coffee (95%) create mode 100644 spec/javascripts/unit/darkswarm/services/order_spec.js.coffee diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index ea8c47eed8..7f09206ea3 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -4,8 +4,10 @@ #= require spin # #= require angular +#= require angular-cookies #= require angular-resource #= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT +#= require ../shared/angular-local-storage.js # #= require ../shared/jquery.timeago #= require foundation diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index f1cd687fa8..538255f74c 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,9 +1,12 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, Order) -> - $scope.require_ship_address = false +Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, Order, storage) -> $scope.order = $scope.Order = Order - $scope.shippingMethodChanged = -> - Order.shippingMethodChanged() + # Binding accordion panel states to local storage + storage.bind $scope, "user" + storage.bind $scope, "details" + storage.bind $scope, "billing" + storage.bind $scope, "shipping" + storage.bind $scope, "payment" $scope.purchase = (event)-> event.preventDefault() diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 6be3a4a155..f50ffcf9ef 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,4 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation']).config ($httpProvider, $tooltipProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' $httpProvider.defaults.headers.common.Accept = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 98d8b8fa42..10c2d5322f 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -1,24 +1,17 @@ Darkswarm.factory 'Order', ($resource, Product, order)-> - - ## I am being clever here - ## order is a JSON object generated in shop/checkout/order.rabl - ## We're extending this to add methods while retaining the data! - new class Order constructor: -> @[name] = method for name, method of order # Clone all data from the order JSON object - # Our shipping_methods comes through as a hash like so: {id: requires_shipping_address} # Here we default to the first shipping method if none is selected - @shipping_method_id ||= Object.keys(@shipping_methods)[0] - @ship_address_same_as_billing = true if @ship_address_same_as_billing == null - @shippingMethodChanged() + @shipping_method_id ||= parseInt(Object.keys(@shipping_methods)[0]) + @ship_address_same_as_billing ?= true shippingMethod: -> @shipping_methods[@shipping_method_id] - shippingMethodChanged: => - @require_ship_address = @shippingMethod().require_ship_address if @shippingMethod() + requireShipAddress: -> + @shippingMethod().require_ship_address shippingPrice: -> @shippingMethod().price @@ -26,7 +19,6 @@ Darkswarm.factory 'Order', ($resource, Product, order)-> paymentMethod: -> @payment_methods[@payment_method_id] - cartTotal: -> @shippingPrice() + @display_total diff --git a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee index c7a4473b03..1bb23446a2 100644 --- a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee @@ -1,14 +1,13 @@ Darkswarm.factory 'OrderCycle', ($resource, Product, orderCycleData) -> class OrderCycle - @order_cycle = orderCycleData || null + @order_cycle = orderCycleData # Object or {} due to RABL @push_order_cycle: -> new $resource("/shop/order_cycle").save {order_cycle_id: @order_cycle.order_cycle_id}, (order_data)-> OrderCycle.order_cycle.orders_close_at = order_data.orders_close_at Product.update() @orders_close_at: -> - if @selected() - @order_cycle.orders_close_at + @order_cycle.orders_close_at if @selected() @selected: -> - @order_cycle != null and !$.isEmptyObject(@order_cycle) and @order_cycle.orders_close_at != undefined + !$.isEmptyObject(@order_cycle) and @order_cycle.orders_close_at? diff --git a/app/assets/javascripts/shared/angular-local-storage.js b/app/assets/javascripts/shared/angular-local-storage.js new file mode 100644 index 0000000000..dfe1e2471c --- /dev/null +++ b/app/assets/javascripts/shared/angular-local-storage.js @@ -0,0 +1,170 @@ +/* + * Angular.js localStorage module + * https://github.com/agrublev/angularLocalStorage + */ + +(function (window, angular, undefined) { + 'use strict'; + + angular.module('angularLocalStorage', ['ngCookies']).factory('storage', ['$parse', '$cookieStore', '$window', '$log', function ($parse, $cookieStore, $window, $log) { + /** + * Global Vars + */ + var storage = (typeof $window.localStorage === 'undefined') ? undefined : $window.localStorage; + var supported = typeof storage !== 'undefined'; + + var privateMethods = { + /** + * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) + * @param res - a string that will be parsed for type + * @returns {*} - whatever the real type of stored value was + */ + parseValue: function (res) { + var val; + try { + val = angular.fromJson(res); + if (typeof val === 'undefined') { + val = res; + } + if (val === 'true') { + val = true; + } + if (val === 'false') { + val = false; + } + if ($window.parseFloat(val) === val && !angular.isObject(val)) { + val = $window.parseFloat(val); + } + } catch (e) { + val = res; + } + return val; + } + }; + + var publicMethods = { + /** + * Set - let's you set a new localStorage key pair set + * @param key - a string that will be used as the accessor for the pair + * @param value - the value of the localStorage item + * @returns {*} - will return whatever it is you've stored in the local storage + */ + set: function (key, value) { + if (!supported) { + try { + $cookieStore.put(key, value); + return value; + } catch(e) { + $log.log('Local Storage not supported, make sure you have angular-cookies enabled.'); + } + } + var saver = angular.toJson(value); + storage.setItem(key, saver); + return privateMethods.parseValue(saver); + }, + + /** + * Get - let's you get the value of any pair you've stored + * @param key - the string that you set as accessor for the pair + * @returns {*} - Object,String,Float,Boolean depending on what you stored + */ + get: function (key) { + if (!supported) { + try { + return privateMethods.parseValue($.cookie(key)); + } catch (e) { + return null; + } + } + var item = storage.getItem(key); + return privateMethods.parseValue(item); + }, + + /** + * Remove - let's you nuke a value from localStorage + * @param key - the accessor value + * @returns {boolean} - if everything went as planned + */ + remove: function (key) { + if (!supported) { + try { + $cookieStore.remove(key); + return true; + } catch (e) { + return false; + } + } + storage.removeItem(key); + return true; + }, + + /** + * Bind - let's you directly bind a localStorage value to a $scope variable + * @param {Angular $scope} $scope - the current scope you want the variable available in + * @param {String} key - the name of the variable you are binding + * @param {Object} opts - (optional) custom options like default value or unique store name + * Here are the available options you can set: + * * defaultValue: the default value + * * storeName: add a custom store key value instead of using the scope variable name + * @returns {*} - returns whatever the stored value is + */ + bind: function ($scope, key, opts) { + var defaultOpts = { + defaultValue: '', + storeName: '' + }; + // Backwards compatibility with old defaultValue string + if (angular.isString(opts)) { + opts = angular.extend({},defaultOpts,{defaultValue:opts}); + } else { + // If no defined options we use defaults otherwise extend defaults + opts = (angular.isUndefined(opts)) ? defaultOpts : angular.extend(defaultOpts,opts); + } + + // Set the storeName key for the localStorage entry + // use user defined in specified + var storeName = opts.storeName || key; + + // If a value doesn't already exist store it as is + if (!publicMethods.get(storeName)) { + publicMethods.set(storeName, opts.defaultValue); + } + + // If it does exist assign it to the $scope value + $parse(key).assign($scope, publicMethods.get(storeName)); + + // Register a listener for changes on the $scope value + // to update the localStorage value + $scope.$watch(key, function (val) { + if (angular.isDefined(val)) { + publicMethods.set(storeName, val); + } + }, true); + + return publicMethods.get(storeName); + }, + /** + * Unbind - let's you unbind a variable from localStorage while removing the value from both + * the localStorage and the local variable and sets it to null + * @param $scope - the scope the variable was initially set in + * @param key - the name of the variable you are unbinding + * @param storeName - (optional) if you used a custom storeName you will have to specify it here as well + */ + unbind: function($scope,key,storeName) { + storeName = storeName || key; + $parse(key).assign($scope, null); + $scope.$watch(key, function () { }); + publicMethods.remove(storeName); + }, + /** + * Clear All - let's you clear out ALL localStorage variables, use this carefully! + */ + clearAll: function() { + storage.clear(); + } + }; + + return publicMethods; + }]); + +})(window, window.angular); diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml new file mode 100644 index 0000000000..20a160aaed --- /dev/null +++ b/app/views/shop/checkout/_authentication.html.haml @@ -0,0 +1,9 @@ +%fieldset + %accordion-group{heading: "User"} + .row + %section#checkout_login + .large-6.columns + = render partial: "shop/checkout/login" + %section#checkout_signup + .large-6.columns + = render partial: "shop/checkout/signup" diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 241a65c4d6..20408d0e44 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -1,5 +1,5 @@ %fieldset#details - %accordion-group + %accordion-group{"is-open" => "details"} %accordion-heading .row .large-6.columns diff --git a/app/views/shop/checkout/_order.rabl b/app/views/shop/checkout/_order.rabl index 4bba881fec..48aef7b11e 100644 --- a/app/views/shop/checkout/_order.rabl +++ b/app/views/shop/checkout/_order.rabl @@ -17,7 +17,6 @@ child current_order.ship_address => :ship_address do attributes :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, :state_id, :zipcode end -# Format here is {id: require_ship_address} node :shipping_methods do Hash[current_order.distributor.shipping_methods.collect { |method| [method.id, { diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 5a93ca6075..62aed1d235 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -1,5 +1,5 @@ %fieldset#shipping - %accordion-group + %accordion-group{"is-open" => "shipping"} %accordion-heading .row .large-6.columns @@ -9,24 +9,19 @@ - for ship_method, i in current_distributor.shipping_methods.uniq .row .large-12.columns - -#= f.radio_button :shipping_method_id, ship_method.id, - -#text: ship_method.name, - -#"ng-change" => "shippingMethodChanged()", - -#"ng-model" => "order.shipping_method_id" %label = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-change" => "order.shippingMethodChanged()", "ng-model" => "order.shipping_method_id" = ship_method.name - #distributor_address.panel{"ng-show" => "!order.require_ship_address"} + #distributor_address.panel{"ng-show" => "!order.requireShipAddress()"} = @order.distributor.distributor_info.andand.html_safe = @order.order_cycle.pickup_time_for(@order.distributor) = @order.order_cycle.pickup_instructions_for(@order.distributor) = f.fields_for :ship_address, @order.ship_address do |sa| - #ship_address{"ng-show" => "order.require_ship_address"} + #ship_address{"ng-show" => "order.requireShipAddress()"} %label = hidden_field_tag "order[ship_address_same_as_billing]", "false" = check_box_tag "order[ship_address_same_as_billing]", true, @order.ship_address_same_as_billing, diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index bca55e2a54..09b2561409 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -10,15 +10,7 @@ %accordion.row{"close-others" => "false"} .large-9.columns - unless spree_current_user - %fieldset - %accordion-group{heading: "User"} - .row - %section#checkout_login - .large-6.columns - = render partial: "shop/checkout/login" - %section#checkout_signup - .large-6.columns - = render partial: "shop/checkout/signup" + = render partial: "shop/checkout/authentication" .row = render partial: "shop/checkout/form" diff --git a/app/views/shop/shop/_order_cycles.html.haml b/app/views/shop/shop/_order_cycles.html.haml index d121bb5fb8..7c5ff51df3 100644 --- a/app/views/shop/shop/_order_cycles.html.haml +++ b/app/views/shop/shop/_order_cycles.html.haml @@ -14,3 +14,5 @@ - else %form.custom = yield :order_cycle_form + + diff --git a/app/views/shop/shop/show.html.haml b/app/views/shop/shop/show.html.haml index f8380c6c39..c036eec016 100644 --- a/app/views/shop/shop/show.html.haml +++ b/app/views/shop/shop/show.html.haml @@ -5,7 +5,7 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", - "popover-placement" => "bottom", "popover" => "testy", "popover-trigger" => "openTrigger"} + "popover-placement" => "bottom", "popover" => "Please select an order cycle", "popover-trigger" => "openTrigger"} %closing{"ng-if" => "OrderCycle.selected()"} Orders close diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index f7c48664f8..7a770cf9ee 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -23,7 +23,6 @@ feature "As a consumer I want to check out my cart", js: true do add_product_to_cart end - context "logged in" do before do login_to_consumer_section @@ -39,7 +38,6 @@ feature "As a consumer I want to check out my cart", js: true do context "logged out" do before do visit "/shop/checkout" - save_and_open_page toggle_accordion "User" end diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee index fae814e4e8..08358a4368 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -5,30 +5,7 @@ describe "CheckoutCtrl", -> beforeEach -> module("Darkswarm") - order = - id: 3102 - shipping_method_id: "7" - ship_address_same_as_billing: true - payment_method_id: null - shipping_methods: - 7: - require_ship_address: true - price: 0.0 - - 25: - require_ship_address: false - price: 13 + order = {} inject ($controller) -> scope = {} - ctrl = $controller 'CheckoutCtrl', {$scope: scope, order: order} - - - it 'Gets the ship address automatically', -> - expect(scope.require_ship_address).toEqual true - - it 'Gets the current shipping price', -> - expect(scope.shippingPrice()).toEqual 0.0 - scope.order.shipping_method_id = 25 - expect(scope.shippingPrice()).toEqual 13 - - + ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: order} diff --git a/spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee similarity index 95% rename from spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee rename to spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee index 9687e72380..f7a65867e8 100644 --- a/spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee @@ -39,6 +39,6 @@ describe 'OrderCycle service', -> it "tells us when no order cycle is selected", -> OrderCycle.order_cycle = null expect(OrderCycle.selected()).toEqual false - OrderCycle.order_cycle = {test: "blah"} + OrderCycle.order_cycle = {orders_close_at: "10 days ago"} expect(OrderCycle.selected()).toEqual true diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee new file mode 100644 index 0000000000..1e0dc387ed --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -0,0 +1,47 @@ +describe 'Order service', -> + Order = null + orderData = null + + beforeEach -> + orderData = { + id: 3102 + payment_method_id: null + shipping_methods: + 7: + require_ship_address: true + price: 0.0 + + 25: + require_ship_address: false + price: 13 + payment_methods: + 99: + test: "foo" + } + angular.module('Darkswarm').value('order', orderData) + module 'Darkswarm' + inject ($injector)-> + Order = $injector.get("Order") + + it "defaults the shipping method to the first", -> + expect(Order.shipping_method_id).toEqual 7 + expect(Order.shippingMethod()).toEqual { require_ship_address : true, price : 0 } + + it "defaults to 'same as billing' for address", -> + expect(Order.ship_address_same_as_billing).toEqual true + + it 'Tracks whether a ship address is required', -> + expect(Order.requireShipAddress()).toEqual true + Order.shipping_method_id = 25 + expect(Order.requireShipAddress()).toEqual false + + it 'Gets the current shipping price', -> + expect(Order.shippingPrice()).toEqual 0.0 + Order.shipping_method_id = 25 + expect(Order.shippingPrice()).toEqual 13 + + it 'Gets the current payment method', -> + expect(Order.paymentMethod()).toEqual null + Order.payment_method_id = 99 + expect(Order.paymentMethod()).toEqual {test: "foo"} + From fca9ba284e5ff72ecb993d788df82a9eaba5072e Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 13:25:35 +1000 Subject: [PATCH 24/30] Setting up a Rake task for Jasmine/Karma --- config/ng-test.conf.js | 5 ++- lib/tasks/karma.rake | 31 +++++++++++++++++++ spec/javascripts/application_spec.js | 5 +++ .../unit/order_cycle_spec.js.coffee | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 lib/tasks/karma.rake create mode 100644 spec/javascripts/application_spec.js diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index b1bddd6532..e8a01b3c9c 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -5,11 +5,11 @@ module.exports = function(config) { frameworks: ['jasmine'], files: [ + APPLICATION_SPEC, 'app/assets/javascripts/shared/jquery-1.8.0.js', // TODO: Can we link to Rails' jquery? - 'app/assets/javascripts/shared/angular.js', - 'app/assets/javascripts/shared/angular-*.js', 'app/assets/javascripts/shared/jquery.timeago.js', 'app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js', + 'app/assets/javascripts/shared/angular-local-storage.js', 'app/assets/javascripts/admin/shared_directives.js.coffee', 'app/assets/javascripts/admin/shared_services.js.coffee', @@ -18,7 +18,6 @@ module.exports = function(config) { 'app/assets/javascripts/admin/bulk_product_update.js.coffee', 'app/assets/javascripts/darkswarm/*.js*', 'app/assets/javascripts/darkswarm/**/*.js*', - 'spec/javascripts/unit/**/*.js*' ], diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake new file mode 100644 index 0000000000..6201c66e61 --- /dev/null +++ b/lib/tasks/karma.rake @@ -0,0 +1,31 @@ +namespace :karma do + task :start => :environment do + with_tmp_config :start + end + + task :run => :environment do + with_tmp_config :start, "--single-run" + end + + private + + def with_tmp_config(command, args = nil) + Tempfile.open('karma_unit.js', Rails.root.join('tmp') ) do |f| + f.write unit_js(application_spec_files) + f.flush + system "karma #{command} #{f.path} #{args}" + end + end + + def application_spec_files + sprockets = Rails.application.assets + sprockets.append_path Rails.root.join("spec/javascripts") + files = Rails.application.assets.find_asset("application_spec.js").to_a.map {|e| e.pathname.to_s } + end + + def unit_js(files) + puts files + unit_js = File.open('config/ng-test.conf.js', 'r').read + unit_js.gsub "APPLICATION_SPEC", "\"#{files.join("\",\n\"")}\"" + end +end diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js new file mode 100644 index 0000000000..ec5fef8832 --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,5 @@ +//= require angular +//= require angular-resource +//= require angular-animate +//= require angular-mocks +//= require angular-cookies diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index d6d0ac882b..4817ca0edd 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -828,4 +828,4 @@ describe 'OrderCycle services', -> expect(order_cycle.outgoing_exchanges[0].enterprise_fees).toEqual [{id: 3}, {id: 4}] expect(order_cycle.incoming_exchanges[0].enterprise_fee_ids).toBeUndefined() expect(order_cycle.outgoing_exchanges[0].enterprise_fee_ids).toBeUndefined() - \ No newline at end of file + From a2f62c158ac21f21bfe2f312766d40c634203675 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 13:40:45 +1000 Subject: [PATCH 25/30] Patching a test and removing our duped Angular libs --- .../admin/order_cycle.js.erb.coffee | 2 +- .../javascripts/shared/angular-mocks.js | 1741 ----------------- .../javascripts/shared/angular-resource.js | 10 - app/assets/javascripts/shared/angular.js | 159 -- .../unit/bulk_order_management_spec.js.coffee | 4 +- .../unit/order_cycle_spec.js.coffee | 4 +- 6 files changed, 6 insertions(+), 1914 deletions(-) delete mode 100644 app/assets/javascripts/shared/angular-mocks.js delete mode 100644 app/assets/javascripts/shared/angular-resource.js delete mode 100644 app/assets/javascripts/shared/angular.js diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 5b018e6548..aadf66af60 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -419,4 +419,4 @@ angular.module('order_cycle', ['ngResource']) if !$(this).is(':checked') scope.$apply -> scope.removeDistributionOfVariant(attrs.ofnSyncDistributions) - ) \ No newline at end of file + ) diff --git a/app/assets/javascripts/shared/angular-mocks.js b/app/assets/javascripts/shared/angular-mocks.js deleted file mode 100644 index aad5452b89..0000000000 --- a/app/assets/javascripts/shared/angular-mocks.js +++ /dev/null @@ -1,1741 +0,0 @@ - -/** - * @license AngularJS v1.0.3 - * (c) 2010-2012 Google, Inc. http://angularjs.org - * License: MIT - * - * TODO(vojta): wrap whole file into closure during build - */ - -/** - * @ngdoc overview - * @name angular.mock - * @description - * - * Namespace from 'angular-mocks.js' which contains testing related code. - */ -angular.mock = {}; - -/** - * ! This is a private undocumented service ! - * - * @name ngMock.$browser - * - * @description - * This service is a mock implementation of {@link ng.$browser}. It provides fake - * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, - * cookies, etc... - * - * The api of this service is the same as that of the real {@link ng.$browser $browser}, except - * that there are several helper methods available which can be used in tests. - */ -angular.mock.$BrowserProvider = function() { - this.$get = function(){ - return new angular.mock.$Browser(); - }; -}; - -angular.mock.$Browser = function() { - var self = this; - - this.isMock = true; - self.$$url = "http://server/"; - self.$$lastUrl = self.$$url; // used by url polling fn - self.pollFns = []; - - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = angular.noop; - self.$$incOutstandingRequestCount = angular.noop; - - - // register url polling fn - - self.onUrlChange = function(listener) { - self.pollFns.push( - function() { - if (self.$$lastUrl != self.$$url) { - self.$$lastUrl = self.$$url; - listener(self.$$url); - } - } - ); - - return listener; - }; - - self.cookieHash = {}; - self.lastCookieHash = {}; - self.deferredFns = []; - self.deferredNextId = 0; - - self.defer = function(fn, delay) { - delay = delay || 0; - self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a,b){ return a.time - b.time;}); - return self.deferredNextId++; - }; - - - self.defer.now = 0; - - - self.defer.cancel = function(deferId) { - var fnIndex; - - angular.forEach(self.deferredFns, function(fn, index) { - if (fn.id === deferId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - self.deferredFns.splice(fnIndex, 1); - return true; - } - - return false; - }; - - - /** - * @name ngMock.$browser#defer.flush - * @methodOf ngMock.$browser - * - * @description - * Flushes all pending requests and executes the defer callbacks. - * - * @param {number=} number of milliseconds to flush. See {@link #defer.now} - */ - self.defer.flush = function(delay) { - if (angular.isDefined(delay)) { - self.defer.now += delay; - } else { - if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length-1].time; - } else { - throw Error('No deferred tasks to be flushed'); - } - } - - while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { - self.deferredFns.shift().fn(); - } - }; - /** - * @name ngMock.$browser#defer.now - * @propertyOf ngMock.$browser - * - * @description - * Current milliseconds mock time. - */ - - self.$$baseHref = ''; - self.baseHref = function() { - return this.$$baseHref; - }; -}; -angular.mock.$Browser.prototype = { - -/** - * @name ngMock.$browser#poll - * @methodOf ngMock.$browser - * - * @description - * run all fns in pollFns - */ - poll: function poll() { - angular.forEach(this.pollFns, function(pollFn){ - pollFn(); - }); - }, - - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - - url: function(url, replace) { - if (url) { - this.$$url = url; - return this; - } - - return this.$$url; - }, - - cookies: function(name, value) { - if (name) { - if (value == undefined) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } - }, - - notifyWhenNoOutstandingRequests: function(fn) { - fn(); - } -}; - - -/** - * @ngdoc object - * @name ngMock.$exceptionHandlerProvider - * - * @description - * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed - * into the `$exceptionHandler`. - */ - -/** - * @ngdoc object - * @name ngMock.$exceptionHandler - * - * @description - * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration - * information. - */ - -angular.mock.$ExceptionHandlerProvider = function() { - var handler; - - /** - * @ngdoc method - * @name ngMock.$exceptionHandlerProvider#mode - * @methodOf ngMock.$exceptionHandlerProvider - * - * @description - * Sets the logging mode. - * - * @param {string} mode Mode of operation, defaults to `rethrow`. - * - * - `rethrow`: If any errors are are passed into the handler in tests, it typically - * means that there is a bug in the application or test, so this mock will - * make these tests fail. - * - `log`: Sometimes it is desirable to test that an error is throw, for this case the `log` mode stores the - * error and allows later assertion of it. - * See {@link ngMock.$log#assertEmpty assertEmpty()} and - * {@link ngMock.$log#reset reset()} - */ - this.mode = function(mode) { - switch(mode) { - case 'rethrow': - handler = function(e) { - throw e; - }; - break; - case 'log': - var errors = []; - - handler = function(e) { - if (arguments.length == 1) { - errors.push(e); - } else { - errors.push([].slice.call(arguments, 0)); - } - }; - - handler.errors = errors; - break; - default: - throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); - } - }; - - this.$get = function() { - return handler; - }; - - this.mode('rethrow'); -}; - - -/** - * @ngdoc service - * @name ngMock.$log - * - * @description - * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays - * (one array per logging level). These arrays are exposed as `logs` property of each of the - * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. - * - */ -angular.mock.$LogProvider = function() { - - function concat(array1, array2, index) { - return array1.concat(Array.prototype.slice.call(array2, index)); - } - - - this.$get = function () { - var $log = { - log: function() { $log.log.logs.push(concat([], arguments, 0)); }, - warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, - info: function() { $log.info.logs.push(concat([], arguments, 0)); }, - error: function() { $log.error.logs.push(concat([], arguments, 0)); } - }; - - /** - * @ngdoc method - * @name ngMock.$log#reset - * @methodOf ngMock.$log - * - * @description - * Reset all of the logging arrays to empty. - */ - $log.reset = function () { - /** - * @ngdoc property - * @name ngMock.$log#log.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.log.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#warn.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.warn.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#info.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.info.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#error.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.error.logs = []; - }; - - /** - * @ngdoc method - * @name ngMock.$log#assertEmpty - * @methodOf ngMock.$log - * - * @description - * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. - */ - $log.assertEmpty = function() { - var errors = []; - angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { - angular.forEach($log[logLevel].logs, function(log) { - angular.forEach(log, function (logItem) { - errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); - }); - }); - }); - if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + - "log message was not checked and removed:"); - errors.push(''); - throw new Error(errors.join('\n---------\n')); - } - }; - - $log.reset(); - return $log; - }; -}; - - -(function() { - var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; - - function jsonStringToDate(string){ - var match; - if (match = string.match(R_ISO8061_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0; - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); - } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); - return date; - } - return string; - } - - function int(str) { - return parseInt(str, 10); - } - - function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; - } - - - /** - * @ngdoc object - * @name angular.mock.TzDate - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. - * - * Mock of the Date type which has its timezone specified via constroctor arg. - * - * The main purpose is to create Date-like instances with timezone fixed to the specified timezone - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - * - *
-   * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
-   * newYearInBratislava.getTimezoneOffset() => -60;
-   * newYearInBratislava.getFullYear() => 2010;
-   * newYearInBratislava.getMonth() => 0;
-   * newYearInBratislava.getDate() => 1;
-   * newYearInBratislava.getHours() => 0;
-   * newYearInBratislava.getMinutes() => 0;
-   * 
- * - */ - angular.mock.TzDate = function (offset, timestamp) { - var self = new Date(0); - if (angular.isString(timestamp)) { - var tsStr = timestamp; - - self.origDate = jsonStringToDate(timestamp); - - timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) - throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" - }; - } else { - self.origDate = new Date(timestamp); - } - - var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; - self.date = new Date(timestamp + self.offsetDiff); - - self.getTime = function() { - return self.date.getTime() - self.offsetDiff; - }; - - self.toLocaleDateString = function() { - return self.date.toLocaleDateString(); - }; - - self.getFullYear = function() { - return self.date.getFullYear(); - }; - - self.getMonth = function() { - return self.date.getMonth(); - }; - - self.getDate = function() { - return self.date.getDate(); - }; - - self.getHours = function() { - return self.date.getHours(); - }; - - self.getMinutes = function() { - return self.date.getMinutes(); - }; - - self.getSeconds = function() { - return self.date.getSeconds(); - }; - - self.getTimezoneOffset = function() { - return offset * 60; - }; - - self.getUTCFullYear = function() { - return self.origDate.getUTCFullYear(); - }; - - self.getUTCMonth = function() { - return self.origDate.getUTCMonth(); - }; - - self.getUTCDate = function() { - return self.origDate.getUTCDate(); - }; - - self.getUTCHours = function() { - return self.origDate.getUTCHours(); - }; - - self.getUTCMinutes = function() { - return self.origDate.getUTCMinutes(); - }; - - self.getUTCSeconds = function() { - return self.origDate.getUTCSeconds(); - }; - - self.getUTCMilliseconds = function() { - return self.origDate.getUTCMilliseconds(); - }; - - self.getDay = function() { - return self.date.getDay(); - }; - - // provide this method only on browsers that already have it - if (self.toISOString) { - self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' - } - } - - //hide all methods not implemented in this mock that the Date prototype exposes - var unimplementedMethods = ['getMilliseconds', 'getUTCDay', - 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', - 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', - 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', - 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', - 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - - angular.forEach(unimplementedMethods, function(methodName) { - self[methodName] = function() { - throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); - }; - }); - - return self; - }; - - //make "tzDateInstance instanceof Date" return true - angular.mock.TzDate.prototype = Date.prototype; -})(); - - -/** - * @ngdoc function - * @name angular.mock.debug - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available function. - * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. - * - * This method is also available on window, where it can be used to display objects on debug console. - * - * @param {*} object - any object to turn into string. - * @return {string} a serialized string of the argument - */ -angular.mock.dump = function(object) { - return serialize(object); - - function serialize(object) { - var out; - - if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
'); - angular.forEach(object, function(element) { - out.append(angular.element(element).clone()); - }); - out = out.html(); - } else if (angular.isArray(object)) { - out = []; - angular.forEach(object, function(o) { - out.push(serialize(o)); - }); - out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { - if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { - out = serializeScope(object); - } else if (object instanceof Error) { - out = object.stack || ('' + object.name + ': ' + object.message); - } else { - out = angular.toJson(object, true); - } - } else { - out = String(object); - } - - return out; - } - - function serializeScope(scope, offset) { - offset = offset || ' '; - var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { - if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { - log.push(' ' + key + ': ' + angular.toJson(scope[key])); - } - } - var child = scope.$$childHead; - while(child) { - log.push(serializeScope(child, offset + ' ')); - child = child.$$nextSibling; - } - log.push('}'); - return log.join('\n' + offset); - } -}; - -/** - * @ngdoc object - * @name ngMock.$httpBackend - * @description - * Fake HTTP backend implementation suitable for unit testing application that use the - * {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less - * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. - * - * During unit testing, we want our unit tests to run quickly and have no external dependencies so - * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is - * to verify whether a certain request has been sent or not, or alternatively just let the - * application make requests, respond with pre-trained responses and assert that the end result is - * what we expect it to be. - * - * This mock implementation can be used to respond with static or dynamic responses via the - * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). - * - * When an Angular application needs some data from a server, it calls the $http service, which - * sends the request to a real server using $httpBackend service. With dependency injection, it is - * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to real server. - * - * There are two ways to specify what test data should be returned as http responses by the mock - * backend when the code under test makes http requests: - * - * - `$httpBackend.expect` - specifies a request expectation - * - `$httpBackend.when` - specifies a backend definition - * - * - * # Request Expectations vs Backend Definitions - * - * Request expectations provide a way to make assertions about requests made by the application and - * to define responses for those requests. The test will fail if the expected requests are not made - * or they are made in the wrong order. - * - * Backend definitions allow you to define a fake backend for your application which doesn't assert - * if a particular request was made or not, it just returns a trained response if a request is made. - * The test will pass whether or not the request gets made during testing. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
- * - * In cases where both backend definitions and request expectations are specified during unit - * testing, the request expectations are evaluated first. - * - * If a request expectation has no response specified, the algorithm will search your backend - * definitions for an appropriate response. - * - * If a request didn't match any expectation or if the expectation doesn't have the response - * defined, the backend definitions are evaluated in sequential order to see if any of them match - * the request. The response from the first matched definition is returned. - * - * - * # Flushing HTTP requests - * - * The $httpBackend used in production, always responds to requests with responses asynchronously. - * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are - * hard to write, follow and maintain. At the same time the testing mock, can't respond - * synchronously because that would change the execution of the code under test. For this reason the - * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending - * requests and thus preserving the async api of the backend, while allowing the test to execute - * synchronously. - * - * - * # Unit testing with mock $httpBackend - * - *
-   // controller
-   function MyController($scope, $http) {
-     $http.get('/auth.py').success(function(data) {
-       $scope.user = data;
-     });
-
-     this.saveMessage = function(message) {
-       $scope.status = 'Saving...';
-       $http.post('/add-msg.py', message).success(function(response) {
-         $scope.status = '';
-       }).error(function() {
-         $scope.status = 'ERROR!';
-       });
-     };
-   }
-
-   // testing controller
-   var $http;
-
-   beforeEach(inject(function($injector) {
-     $httpBackend = $injector.get('$httpBackend');
-
-     // backend definition common for all tests
-     $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
-   }));
-
-
-   afterEach(function() {
-     $httpBackend.verifyNoOutstandingExpectation();
-     $httpBackend.verifyNoOutstandingRequest();
-   });
-
-
-   it('should fetch authentication token', function() {
-     $httpBackend.expectGET('/auth.py');
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-   });
-
-
-   it('should send msg to server', function() {
-     // now you don’t care about the authentication, but
-     // the controller will still send the request and
-     // $httpBackend will respond without you having to
-     // specify the expectation and response for this request
-     $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
-
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-     controller.saveMessage('message content');
-     expect(controller.status).toBe('Saving...');
-     $httpBackend.flush();
-     expect(controller.status).toBe('');
-   });
-
-
-   it('should send auth header', function() {
-     $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
-       // check if the header was send, if it wasn't the expectation won't
-       // match the request and the test will fail
-       return headers['Authorization'] == 'xxx';
-     }).respond(201, '');
-
-     var controller = scope.$new(MyController);
-     controller.saveMessage('whatever');
-     $httpBackend.flush();
-   });
-   
- */ -angular.mock.$HttpBackendProvider = function() { - this.$get = [createHttpBackendMock]; -}; - -/** - * General factory function for $httpBackend mock. - * Returns instance for unit testing (when no arguments specified): - * - passing through is disabled - * - auto flushing is disabled - * - * Returns instance for e2e testing (when `$delegate` and `$browser` specified): - * - passing through (delegating request to real backend) is enabled - * - auto flushing is enabled - * - * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) - * @param {Object=} $browser Auto-flushing enabled if specified - * @return {Object} Instance of $httpBackend mock - */ -function createHttpBackendMock($delegate, $browser) { - var definitions = [], - expectations = [], - responses = [], - responsesPush = angular.bind(responses, responses.push); - - function createResponse(status, data, headers) { - if (angular.isFunction(status)) return status; - - return function() { - return angular.isNumber(status) - ? [status, data, headers] - : [200, status, data]; - }; - } - - // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers) { - var xhr = new MockXhr(), - expectation = expectations[0], - wasExpected = false; - - function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) - ? data - : angular.toJson(data); - } - - if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) - throw Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); - - if (!expectation.matchHeaders(headers)) - throw Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); - - expectations.shift(); - - if (expectation.response) { - responses.push(function() { - var response = expectation.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); - }); - return; - } - wasExpected = true; - } - - var i = -1, definition; - while ((definition = definitions[++i])) { - if (definition.match(method, url, data, headers || {})) { - if (definition.response) { - // if $browser specified, we do auto flush all requests - ($browser ? $browser.defer : responsesPush)(function() { - var response = definition.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); - }); - } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers); - } else throw Error('No response defined !'); - return; - } - } - throw wasExpected ? - Error('No response defined !') : - Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); - } - - /** - * @ngdoc method - * @name ngMock.$httpBackend#when - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), - chain = { - respond: function(status, data, headers) { - definition.response = createResponse(status, data, headers); - } - }; - - if ($browser) { - chain.passThrough = function() { - definition.passThrough = true; - }; - } - - definitions.push(definition); - return chain; - }; - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenGET - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenHEAD - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenDELETE - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenPOST - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenPUT - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenJSONP - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('when'); - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expect - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers); - expectations.push(expectation); - return { - respond: function(status, data, headers) { - expectation.response = createResponse(status, data, headers); - } - }; - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectGET - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for GET requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. See #expect for more info. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectHEAD - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for HEAD requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectDELETE - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for DELETE requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPOST - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for POST requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPUT - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for PUT requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPATCH - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for PATCH requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectJSONP - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for JSONP requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('expect'); - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#flush - * @methodOf ngMock.$httpBackend - * @description - * Flushes all pending requests using the trained responses. - * - * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, - * all pending requests will be flushed. If there are no pending requests when the flush method - * is called an exception is thrown (as this typically a sign of programming error). - */ - $httpBackend.flush = function(count) { - if (!responses.length) throw Error('No pending request to flush !'); - - if (angular.isDefined(count)) { - while (count--) { - if (!responses.length) throw Error('No more pending request to flush !'); - responses.shift()(); - } - } else { - while (responses.length) { - responses.shift()(); - } - } - $httpBackend.verifyNoOutstandingExpectation(); - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingExpectation - * @methodOf ngMock.$httpBackend - * @description - * Verifies that all of the requests defined via the `expect` api were made. If any of the - * requests were not made, verifyNoOutstandingExpectation throws an exception. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - *
-   *   afterEach($httpBackend.verifyExpectations);
-   * 
- */ - $httpBackend.verifyNoOutstandingExpectation = function() { - if (expectations.length) { - throw Error('Unsatisfied requests: ' + expectations.join(', ')); - } - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingRequest - * @methodOf ngMock.$httpBackend - * @description - * Verifies that there are no outstanding requests that need to be flushed. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - *
-   *   afterEach($httpBackend.verifyNoOutstandingRequest);
-   * 
- */ - $httpBackend.verifyNoOutstandingRequest = function() { - if (responses.length) { - throw Error('Unflushed requests: ' + responses.length); - } - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#resetExpectations - * @methodOf ngMock.$httpBackend - * @description - * Resets all request expectations, but preserves all backend definitions. Typically, you would - * call resetExpectations during a multiple-phase test when you want to reuse the same instance of - * $httpBackend mock. - */ - $httpBackend.resetExpectations = function() { - expectations.length = 0; - responses.length = 0; - }; - - return $httpBackend; - - - function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers) - } - }); - - angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers) - } - }); - } -} - -function MockHttpExpectation(method, url, data, headers) { - - this.data = data; - this.headers = headers; - - this.match = function(m, u, d, h) { - if (method != m) return false; - if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; - return true; - }; - - this.matchUrl = function(u) { - if (!url) return true; - if (angular.isFunction(url.test)) return url.test(u); - return url == u; - }; - - this.matchHeaders = function(h) { - if (angular.isUndefined(headers)) return true; - if (angular.isFunction(headers)) return headers(h); - return angular.equals(headers, h); - }; - - this.matchData = function(d) { - if (angular.isUndefined(data)) return true; - if (data && angular.isFunction(data.test)) return data.test(d); - if (data && !angular.isString(data)) return angular.toJson(data) == d; - return data == d; - }; - - this.toString = function() { - return method + ' ' + url; - }; -} - -function MockXhr() { - - // hack for testing $http, $httpBackend - MockXhr.$$lastInstance = this; - - this.open = function(method, url, async) { - this.$$method = method; - this.$$url = url; - this.$$async = async; - this.$$reqHeaders = {}; - this.$$respHeaders = {}; - }; - - this.send = function(data) { - this.$$data = data; - }; - - this.setRequestHeader = function(key, value) { - this.$$reqHeaders[key] = value; - }; - - this.getResponseHeader = function(name) { - // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last - var header = this.$$respHeaders[name]; - if (header) return header; - - name = angular.lowercase(name); - header = this.$$respHeaders[name]; - if (header) return header; - - header = undefined; - angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) == name) header = headerVal; - }); - return header; - }; - - this.getAllResponseHeaders = function() { - var lines = []; - - angular.forEach(this.$$respHeaders, function(value, key) { - lines.push(key + ': ' + value); - }); - return lines.join('\n'); - }; - - this.abort = angular.noop; -} - - -/** - * @ngdoc function - * @name ngMock.$timeout - * @description - * - * This service is just a simple decorator for {@link ng.$timeout $timeout} service - * that adds a "flush" method. - */ - -/** - * @ngdoc method - * @name ngMock.$timeout#flush - * @methodOf ngMock.$timeout - * @description - * - * Flushes the queue of pending tasks. - */ - -/** - * - */ -angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
'); - } -}; - -/** - * @ngdoc overview - * @name ngMock - * @description - * - * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful - * mocks to the {@link AUTO.$injector $injector}. - */ -angular.module('ngMock', ['ng']).provider({ - $browser: angular.mock.$BrowserProvider, - $exceptionHandler: angular.mock.$ExceptionHandlerProvider, - $log: angular.mock.$LogProvider, - $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider -}).config(function($provide) { - $provide.decorator('$timeout', function($delegate, $browser) { - $delegate.flush = function() { - $browser.defer.flush(); - }; - return $delegate; - }); -}); - - -/** - * @ngdoc overview - * @name ngMockE2E - * @description - * - * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. - * Currently there is only one mock present in this module - - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. - */ -angular.module('ngMockE2E', ['ng']).config(function($provide) { - $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}); - -/** - * @ngdoc object - * @name ngMockE2E.$httpBackend - * @description - * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of - * applications that use the {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for unit testing please see - * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. - * - * This implementation can be used to respond with static or dynamic responses via the `when` api - * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the - * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch - * templates from a webserver). - * - * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application - * is being developed with the real backend api replaced with a mock, it is often desirable for - * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch - * templates or static files from the webserver). To configure the backend with this behavior - * use the `passThrough` request handler of `when` instead of `respond`. - * - * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests - * automatically, closely simulating the behavior of the XMLHttpRequest object. - * - * To setup the application to run with this http backend, you have to create a module that depends - * on the `ngMockE2E` and your application modules and defines the fake backend: - * - *
- *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
- *   myAppDev.run(function($httpBackend) {
- *     phones = [{name: 'phone1'}, {name: 'phone2'}];
- *
- *     // returns the current list of phones
- *     $httpBackend.whenGET('/phones').respond(phones);
- *
- *     // adds a new phone to the phones array
- *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
- *       phones.push(angular.fromJSON(data));
- *     });
- *     $httpBackend.whenGET(/^\/templates\//).passThrough();
- *     //...
- *   });
- * 
- * - * Afterwards, bootstrap your app with this new module. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#when - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` - * handler, will be pass through to the real backend (an XHR request will be made to the - * server. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenGET - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenHEAD - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenDELETE - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPOST - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPUT - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPATCH - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for PATCH requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenJSONP - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ -angular.mock.e2e = {}; -angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; - - -angular.mock.clearDataCache = function() { - var key, - cache = angular.element.cache; - - for(key in cache) { - if (cache.hasOwnProperty(key)) { - var handle = cache[key].handle; - - handle && angular.element(handle.elem).unbind(); - delete cache[key]; - } - } -}; - - -window.jstestdriver && (function(window) { - /** - * Global method to output any number of objects into JSTD console. Useful for debugging. - */ - window.dump = function() { - var args = []; - angular.forEach(arguments, function(arg) { - args.push(angular.mock.dump(arg)); - }); - jstestdriver.console.log.apply(jstestdriver.console, args); - if (window.console) { - window.console.log.apply(window.console, args); - } - }; -})(window); - - -window.jasmine && (function(window) { - - afterEach(function() { - var spec = getCurrentSpec(); - var injector = spec.$injector; - - spec.$injector = null; - spec.$modules = null; - - if (injector) { - injector.get('$rootElement').unbind(); - injector.get('$browser').pollFns.length = 0; - } - - angular.mock.clearDataCache(); - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - - function getCurrentSpec() { - return jasmine.getEnv().currentSpec; - } - - function isSpecRunning() { - var spec = getCurrentSpec(); - return spec && spec.queue.running; - } - - /** - * @ngdoc function - * @name angular.mock.module - * @description - * - * *NOTE*: This is function is also published on window for easy access.
- * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. - * - * This function registers a module configuration code. It collects the configuration information - * which will be used when the injector is created by {@link angular.mock.inject inject}. - * - * See {@link angular.mock.inject inject} for usage example - * - * @param {...(string|Function)} fns any number of modules which are represented as string - * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. - */ - window.module = angular.mock.module = function() { - var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - var spec = getCurrentSpec(); - if (spec.$injector) { - throw Error('Injector already created, can not register a module!'); - } else { - var modules = spec.$modules || (spec.$modules = []); - angular.forEach(moduleFns, function(module) { - modules.push(module); - }); - } - } - }; - - /** - * @ngdoc function - * @name angular.mock.inject - * @description - * - * *NOTE*: This is function is also published on window for easy access.
- * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. - * - * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link AUTO.$injector $injector} per test, which is then used for - * resolving references. - * - * See also {@link angular.mock.module module} - * - * Example of what a typical jasmine tests looks like with the inject method. - *
-   *
-   *   angular.module('myApplicationModule', [])
-   *       .value('mode', 'app')
-   *       .value('version', 'v1.0.1');
-   *
-   *
-   *   describe('MyApp', function() {
-   *
-   *     // You need to load modules that you want to test,
-   *     // it loads only the "ng" module by default.
-   *     beforeEach(module('myApplicationModule'));
-   *
-   *
-   *     // inject() is used to inject arguments of all given functions
-   *     it('should provide a version', inject(function(mode, version) {
-   *       expect(version).toEqual('v1.0.1');
-   *       expect(mode).toEqual('app');
-   *     }));
-   *
-   *
-   *     // The inject and module method can also be used inside of the it or beforeEach
-   *     it('should override a version and test the new version is injected', function() {
-   *       // module() takes functions or strings (module aliases)
-   *       module(function($provide) {
-   *         $provide.value('version', 'overridden'); // override version here
-   *       });
-   *
-   *       inject(function(version) {
-   *         expect(version).toEqual('overridden');
-   *       });
-   *     ));
-   *   });
-   *
-   * 
- * - * @param {...Function} fns any number of functions which will be injected using the injector. - */ - window.inject = angular.mock.inject = function() { - var blockFns = Array.prototype.slice.call(arguments, 0); - var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - var spec = getCurrentSpec(); - var modules = spec.$modules || []; - modules.unshift('ngMock'); - modules.unshift('ng'); - var injector = spec.$injector; - if (!injector) { - injector = spec.$injector = angular.injector(modules); - } - for(var i = 0, ii = blockFns.length; i < ii; i++) { - try { - injector.invoke(blockFns[i] || angular.noop, this); - } catch (e) { - if(e.stack) e.stack += '\n' + errorForStack.stack; - throw e; - } finally { - errorForStack = null; - } - } - } - }; -})(window); diff --git a/app/assets/javascripts/shared/angular-resource.js b/app/assets/javascripts/shared/angular-resource.js deleted file mode 100644 index 816107d241..0000000000 --- a/app/assets/javascripts/shared/angular-resource.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - AngularJS v1.0.3 - (c) 2010-2012 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(A,e,w){'use strict';e.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(x,y){function k(a,f){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(f?null:/%20/g,"+")}function t(a,f){this.template=a+="#";this.defaults=f||{};var b=this.urlParams={};l(a.split(/\W/),function(d){d&&a.match(RegExp("[^\\\\]:"+d+"\\W"))&&(b[d]=!0)});this.template=a.replace(/\\:/g,":")}function u(a,f,b){function d(b,c){var a= -{},c=i({},f,c);l(c,function(o,c){var d;o.charAt&&o.charAt(0)=="@"?(d=o.substr(1),d=y(d)(b)):d=o;a[c]=d});return a}function h(a){v(a||{},this)}var e=new t(a),b=i({},z,b);l(b,function(g,c){var k=g.method=="POST"||g.method=="PUT"||g.method=="PATCH";h[c]=function(a,b,c,f){var s={},j,m=p,q=null;switch(arguments.length){case 4:q=f,m=c;case 3:case 2:if(r(b)){if(r(a)){m=a;q=b;break}m=b;q=c}else{s=a;j=b;m=c;break}case 1:r(a)?m=a:k?j=a:s=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ -arguments.length+" arguments.";}var n=this instanceof h?this:g.isArray?[]:new h(j);x({method:g.method,url:e.url(i({},d(j,g.params||{}),s)),data:j}).then(function(a){var b=a.data;if(b)g.isArray?(n.length=0,l(b,function(a){n.push(new h(a))})):v(b,n);(m||p)(n,a.headers)},q);return n};h.bind=function(c){return u(a,i({},f,c),b)};h.prototype["$"+c]=function(a,b,f){var g=d(this),e=p,j;switch(arguments.length){case 3:g=a;e=b;j=f;break;case 2:case 1:r(a)?(e=a,j=b):(g=a,e=b||p);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ -arguments.length+" arguments.";}h[c].call(this,g,k?this:w,e,j)}});return h}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},p=e.noop,l=e.forEach,i=e.extend,v=e.copy,r=e.isFunction;t.prototype={url:function(a){var f=this,b=this.template,d,h,a=a||{};l(this.urlParams,function(g,c){d=a.hasOwnProperty(c)?a[c]:f.defaults[c];e.isDefined(d)&&d!==null?(h=k(d,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"), -b=b.replace(RegExp(":"+c+"(\\W)","g"),h+"$1")):b=b.replace(RegExp("/?:"+c+"(\\W)","g"),"$1")});var b=b.replace(/\/?#$/,""),i=[];l(a,function(a,b){f.urlParams[b]||i.push(k(b)+"="+k(a))});i.sort();b=b.replace(/\/*$/,"");return b+(i.length?"?"+i.join("&"):"")}};return u}])})(window,window.angular); diff --git a/app/assets/javascripts/shared/angular.js b/app/assets/javascripts/shared/angular.js deleted file mode 100644 index 07f501be38..0000000000 --- a/app/assets/javascripts/shared/angular.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - AngularJS v1.0.3 - (c) 2010-2012 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(U,ca,p){'use strict';function m(b,a,c){var d;if(b)if(N(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(L(b)&&wa(b.length))for(d=0;d=0&&b.splice(c,1);return a}function V(b,a){if(oa(b)||b&&b.$evalAsync&&b.$watch)throw B("Can't copy Window or Scope");if(a){if(b=== -a)throw B("Can't copy equivalent objects or arrays");if(J(b)){for(;a.length;)a.pop();for(var c=0;c2?ia.call(arguments,2):[];return N(a)&&!(a instanceof RegExp)?c.length? -function(){return arguments.length?a.apply(b,c.concat(ia.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function ic(b,a){var c=a;/^\$+/.test(b)?c=p:oa(a)?c="$WINDOW":a&&ca===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function da(b,a){return JSON.stringify(b,ic,a?" ":null)}function nb(b){return F(b)?JSON.parse(b):b}function Wa(b){b&&b.length!==0?(b=E(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1; -return b}function pa(b){b=u(b).clone();try{b.html("")}catch(a){}return u("
").append(b).html().match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+E(b)})}function Xa(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]),a[d]=v(c[1])?decodeURIComponent(c[1]):!0)});return a}function ob(b){var a=[];m(b,function(b,d){a.push(Ya(d,!0)+(b===!0?"":"="+Ya(b,!0)))});return a.length?a.join("&"):""}function Za(b){return Ya(b,!0).replace(/%26/gi,"&").replace(/%3D/gi, -"=").replace(/%2B/gi,"+")}function Ya(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(a?null:/%20/g,"+")}function jc(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,i=["ng:app","ng-app","x-ng-app","data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(i,function(a){i[a]=!0;c(ca.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+ -a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&i[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function pb(b,a){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");var c=qb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,i){a.$apply(function(){b.data("$injector",i);c(b)(a)})}]);return c}function $a(b,a){a=a||"_";return b.replace(kc, -function(b,d){return(d?a:"")+b.toLowerCase()})}function qa(b,a,c){if(!b)throw new B("Argument '"+(a||"?")+"' is "+(c||"required"));return b}function ra(b,a,c){c&&J(b)&&(b=b[b.length-1]);qa(N(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function lc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c, -d,e){return function(){b[e||"push"]([c,d,arguments]);return j}}if(!e)throw B("No module: "+d);var b=[],c=[],k=a("$injector","invoke"),j={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:k,run:function(a){c.push(a); -return this}};g&&k(g);return j})}})}function rb(b){return b.replace(mc,function(a,b,d,e){return e?d.toUpperCase():d}).replace(nc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,i,f,h,k,j,l;b.length;){i=b.shift();f=0;for(h=i.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function sa(b){sb(b);for(var a=0,b=b.childNodes||[];a-1}function vb(b,a){a&&m(a.split(" "),function(a){b.className= -R((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+R(a)+" "," "))})}function wb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=R(b.className+" "+R(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&v(a.length)&&!oa(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"),W.length>20&&c.warn("Cookie '"+a+"' possibly not set or overflowed because too many cookies were already set ("+W.length+ -" > 20 )")}else{if(h.cookie!==y){y=h.cookie;d=y.split("; ");W={};for(f=0;f0&&(W[unescape(e.substring(0,k))]=unescape(e.substring(k+1)))}return W}};f.defer=function(a,b){var c;n++;c=l(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};f.defer.cancel=function(a){return r[a]?(delete r[a],o(a),e(D),!0):!1}}function wc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new vc(b,d,a,c)}]}function xc(){this.$get=function(){function b(b, -d){function e(a){if(a!=l){if(o){if(o==a)o=a.n}else o=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw B("cacheId "+b+" taken");var i=0,f=x({},d,{id:b}),h={},k=d&&d.capacity||Number.MAX_VALUE,j={},l=null,o=null;return a[b]={put:function(a,b){var c=j[a]||(j[a]={key:a});e(c);t(b)||(a in h||i++,h[a]=b,i>k&&this.remove(o.key))},get:function(a){var b=j[a];if(b)return e(b),h[a]},remove:function(a){var b=j[a];if(b){if(b==l)l=b.p;if(b==o)o=b.n;g(b.n,b.p);delete j[a]; -delete h[a];i--}},removeAll:function(){h={};i=0;j={};l=o=null},destroy:function(){j=f=h=null;delete a[b]},info:function(){return x({},f,{size:i})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function yc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Bb(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: "; -this.directive=function f(d,e){F(d)?(qa(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d],function(a){try{var f=b.invoke(a);if(N(f))f={compile:I(f)};else if(!f.compile&&f.link)f.compile=I(f.link);f.priority=f.priority||0;f.name=f.name||d;f.require=f.require||f.controller&&f.name;f.restrict=f.restrict||"A";e.push(f)}catch(k){c(k)}});return e}])),a[d].push(e)):m(d,mb(f));return this};this.$get=["$injector","$interpolate","$exceptionHandler", -"$http","$templateCache","$parse","$controller","$rootScope",function(b,h,k,j,l,o,r,n){function w(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&(a[c]=u(b).wrap("").parent()[0])});var d=s(a,b,a,c);return function(b,c){qa(b,"scope");var e=c?ua.clone.call(a):a;e.data("$scope",b);q(e,"ng-scope");c&&c(e,b);d&&d(b,e,e);return e}}function q(a,b){try{a.addClass(b)}catch(c){}}function s(a,b,c,d){function e(a,c,d,k){for(var g,h,j,n,o,l=0,r=0,q=f.length;lz.priority)break;if(Y=z.scope)M("isolated scope",C,z,y),L(Y)&&(q(y,"ng-isolate-scope"),C=z),q(y,"ng-scope"),s=s||z;H=z.name;if(Y=z.controller)t=t||{},M("'"+H+"' controller",t[H],z,y),t[H]=z;if(Y=z.transclude)M("transclusion",D,z,y),D=z,n=z.priority,Y=="element"?(X=u(b),y=c.$$element=u("<\!-- "+H+": "+c[H]+" --\>"),b=y[0],Ga(e,u(X[0]),b),v=w(X,d,n)):(X=u(cb(b)).contents(),y.html(""),v=w(X,d));if(Y=z.template)if(M("template",A,z,y),A=z,Y=Ha(Y),z.replace){X=u("
"+R(Y)+"
").contents(); -b=X[0];if(X.length!=1||b.nodeType!==1)throw new B(g+Y);Ga(e,y,b);H={$attr:{}};a=a.concat(O(b,a.splice(E+1,a.length-(E+1)),H));K(c,H);G=a.length}else y.html(Y);if(z.templateUrl)M("template",A,z,y),A=z,j=W(a.splice(E,a.length-E),j,y,c,e,z.replace,v),G=a.length;else if(z.compile)try{x=z.compile(y,c,v),N(x)?f(null,x):x&&f(x.pre,x.post)}catch(I){k(I,pa(y))}if(z.terminal)j.terminal=!0,n=Math.max(n,z.priority)}j.scope=s&&s.scope;j.transclude=D&&v;return j}function A(d,e,g,h){var j=!1;if(a.hasOwnProperty(e))for(var n, -e=b.get(e+c),o=0,l=e.length;on.priority)&&n.restrict.indexOf(g)!=-1)d.push(n),j=!0}catch(r){k(r)}return j}function K(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,function(b,f){f=="class"?(q(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):f=="style"?e.attr("style",e.attr("style")+";"+b):f.charAt(0)!="$"&&!a.hasOwnProperty(f)&&(a[f]=b,d[f]=c[f])})}function W(a,b,c,d,e, -f,k){var h=[],n,o,r=c[0],q=a.shift(),w=x({},q,{controller:null,templateUrl:null,transclude:null,scope:null});c.html("");j.get(q.templateUrl,{cache:l}).success(function(j){var l,q,j=Ha(j);if(f){q=u("
"+R(j)+"
").contents();l=q[0];if(q.length!=1||l.nodeType!==1)throw new B(g+j);j={$attr:{}};Ga(e,c,l);O(l,a,j);K(d,j)}else l=r,c.html(j);a.unshift(w);n=C(a,c,d,k);for(o=s(c.contents(),k);h.length;){var ba=h.pop(),j=h.pop();q=h.pop();var y=h.pop(),m=l;q!==r&&(m=cb(l),Ga(j,u(q),m));n(function(){b(o, -y,m,e,ba)},y,m,e,ba)}h=null}).error(function(a,b,c,d){throw B("Failed to load template: "+d.url);});return function(a,c,d,e,f){h?(h.push(c),h.push(d),h.push(e),h.push(f)):n(function(){b(o,c,d,e,f)},c,d,e,f)}}function y(a,b){return b.priority-a.priority}function M(a,b,c,d){if(b)throw B("Multiple directives ["+b.name+", "+c.name+"] asking for "+a+" on: "+pa(d));}function H(a,b){var c=h(b,!0);c&&a.push({priority:0,compile:I(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);q(d.data("$binding", -e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function X(a,b,c,d){var e=h(c,!0);e&&b.push({priority:100,compile:I(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=h(c[d],!0));c[d]=p;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e,function(a){c.$set(d,a)})})})}function Ga(a,b,c){var d=b[0],e=d.parentNode,f,g;if(a){f=0;for(g=a.length;f0){var e=M[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=i(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),M.shift(),b):!1}function h(a){f(a)||e("is unexpected, expecting ["+a+"]",i())}function k(a,b){return function(c,d){return a(c,d,b)}}function j(a,b,c){return function(d,e){return b(d,e,a,c)}}function l(){for(var a=[];;)if(M.length>0&&!i("}",")",";","]")&&a.push(v()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d, -e=0;e","<=",">="))a=j(a,b.fn,q());return a}function s(){for(var a=m(),b;b=f("*","/","%");)a=j(a,b.fn,m());return a}function m(){var a;return f("+")?C():(a=f("-"))?j(W,a.fn,m()):(a=f("!"))?k(a.fn,m()):C()}function C(){var a;if(f("("))a=v(),h(")");else if(f("["))a=A();else if(f("{"))a=K();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=u(a,c),c=null):b.text==="["?(c=a,a=ea(a)):b.text==="."?(c=a,a=t(a)):e("IMPOSSIBLE"); -return a}function A(){var a=[];if(g().text!="]"){do a.push(H());while(f(","))}h("]");return function(b,c){for(var d=[],e=0;e1;d++){var e=a.shift(),g= -b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]=c}function fb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,i=0;i7),hasEvent:function(c){if(c=="input"&&aa==9)return!1;if(t(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Uc(){this.$get=I(U)}function Mb(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=E(R(b.substr(0, -e)));d=R(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Nb(b){var a=L(b)?b:p;return function(c){a||(a=Mb(b));return c?a[E(c)]||null:a}}function Ob(b,a,c){if(N(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Vc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){F(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=nb(d,!0)));return d}],transformRequest:[function(a){return L(a)&&Sa.apply(a)!=="[object File]"?da(a):a}], -headers:{common:{Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,h,k,j){function l(a){function c(a){var b=x({},a,{data:Ob(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:k.reject(b)}a.method=la(a.method);var e=a.transformRequest|| -d.transformRequest,f=a.transformResponse||d.transformResponse,h=d.headers,h=x({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},h.common,h[E(a.method)],a.headers),e=Ob(a.data,Nb(h),e),g;t(a.data)&&delete h["Content-Type"];g=o(a,e,h);g=g.then(c,c);m(w,function(a){g=a(g)});g.success=function(b){g.then(function(c){b(c.data,c.status,c.headers,a)});return g};g.error=function(b){g.then(null,function(c){b(c.data,c.status,c.headers,a)});return g};return g}function o(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(w, -[a,b,Mb(c)]):m.remove(w));f(b,a,c);h.$apply()}function f(a,c,d){c=Math.max(c,0);(200<=c&&c<300?j.resolve:j.reject)({data:a,status:c,headers:Nb(d),config:b})}function i(){var a=za(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var j=k.defer(),o=j.promise,m,p,w=r(b.url,b.params);l.pendingRequests.push(b);o.then(i,i);b.cache&&b.method=="GET"&&(m=L(b.cache)?b.cache:n);if(m)if(p=m.get(w))if(p.then)return p.then(i,i),p;else J(p)?f(p[1],p[0],V(p[2])):f(p,200,{});else m.put(w,o);p||a(b.method, -w,c,e,d,b.timeout,b.withCredentials);return o}function r(a,b){if(!b)return a;var c=[];ec(b,function(a,b){a==null||a==p||(L(a)&&(a=da(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var n=c("$http"),w=[];m(e,function(a){w.push(F(a)?j.get(a):j.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(x(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]= -function(b,c,d){return l(x(d||{},{method:a,url:b,data:c}))}})})("post","put");l.defaults=d;return l}]}function Wc(){this.$get=["$browser","$window","$document",function(b,a,c){return Xc(b,Yc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Xc(b,a,c,d,e,g){function i(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;aa?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= -d;e.body.appendChild(c)}return function(e,h,k,j,l,o,r){function n(a,c,d,e){c=(h.match(Fb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(D)}b.$$incOutstandingRequestCount();h=h||b.url();if(E(e)=="jsonp"){var p="_"+(d.counter++).toString(36);d[p]=function(a){d[p].data=a};i(h.replace("JSON_CALLBACK","angular.callbacks."+p),function(){d[p].data?n(j,200,d[p].data):n(j,-2);delete d[p]})}else{var q=new a;q.open(e,h,!0);m(l,function(a,b){a&&q.setRequestHeader(b,a)}); -var s;q.onreadystatechange=function(){q.readyState==4&&n(j,s||q.status,q.responseText,q.getAllResponseHeaders())};if(r)q.withCredentials=!0;q.send(k||"");o>0&&c(function(){s=-1;q.abort()},o)}}}function Zc(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"}, -DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","),SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a", -shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function $c(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,f,h){var k=c.defer(),j=k.promise,l=v(h)&&!h,f=a.defer(function(){try{k.resolve(e())}catch(a){k.reject(a),d(a)}l||b.$apply()},f),h=function(){delete g[j.$$timeoutId]};j.$$timeoutId=f;g[f]=k;j.then(h,h);return j}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)): -!1};return e}]}function Pb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Qb);a("date",Rb);a("filter",ad);a("json",bd);a("limitTo",cd);a("lowercase",dd);a("number",Sb);a("orderBy",Tb);a("uppercase",ed)}function ad(){return function(b,a){if(!(b instanceof Array))return b;var c=[];c.check=function(a){for(var b=0;b-1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;ce+1?i="0":(f=i,k=!0)}if(!k){i=(i.split(Vb)[1]||"").length;t(e)&&(e=Math.min(Math.max(a.minFrac,i),a.maxFrac));var i=Math.pow(10,e),b=Math.round(b*i)/i,b=(""+b).split(Vb),i=b[0],b=b[1]||"",k=0,j=a.lgSize,l=a.gSize;if(i.length>=j+l)for(var k=i.length-j,o=0;o0||e>-c)e+=c;e===0&&c==-12&&(e=12);return ib(e,a,d)}}function La(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Rb(b){function a(a){var b; -if(b=a.match(c)){var a=new Date(0),g=0,i=0;b[9]&&(g=G(b[9]+b[10]),i=G(b[9]+b[11]));a.setUTCFullYear(G(b[1]),G(b[2])-1,G(b[3]));a.setUTCHours(G(b[4]||0)-g,G(b[5]||0)-i,G(b[6]||0),G(b[7]||0))}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e){var g="",i=[],f,h,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;F(c)&&(c=fd.test(c)?G(c):a(c));wa(c)&&(c=new Date(c));if(!na(c))return c;for(;e;)(h=gd.exec(e))?(i=i.concat(ia.call(h, -1)),e=i.pop()):(i.push(e),e=null);m(i,function(a){f=hd[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function bd(){return function(b){return da(b,!0)}}function cd(){return function(b,a){if(!(b instanceof Array))return b;var a=G(a),c=[],d,e;if(!b||!(b instanceof Array))return c;a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dl?(d.$setValidity("maxlength", -!1),p):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function jb(b,a){b="ngClass"+b;return S(function(c,d,e){function g(b,d){if(a===!0||c.$index%2===a)d&&b!==d&&i(d),f(b)}function i(a){L(a)&&!J(a)&&(a=Ta(a,function(a,b){if(a)return b}));d.removeClass(J(a)?a.join(" "):a)}function f(a){L(a)&&!J(a)&&(a=Ta(a,function(a,b){if(a)return b}));a&&d.addClass(J(a)?a.join(" "):a)}c.$watch(e[b],g,!0);e.$observe("class",function(){var a=c.$eval(e[b]);g(a,a)});b!=="ngClass"&&c.$watch("$index", -function(d,g){var j=d%2;j!==g%2&&(j==a?f(c.$eval(e[b])):i(c.$eval(e[b])))})})}var E=function(b){return F(b)?b.toLowerCase():b},la=function(b){return F(b)?b.toUpperCase():b},B=U.Error,aa=G((/msie (\d+)/.exec(E(navigator.userAgent))||[])[1]),u,ja,ia=[].slice,Ra=[].push,Sa=Object.prototype.toString,Yb=U.angular||(U.angular={}),ta,Cb,Z=["0","0","0"];D.$inject=[];ma.$inject=[];Cb=aa<9?function(b){b=b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName? -b.nodeName:b[0].nodeName};var kc=/[A-Z]/g,id={full:"1.0.3",major:1,minor:0,dot:3,codeName:"bouncy-thunder"},Ba=Q.cache={},Aa=Q.expando="ng-"+(new Date).getTime(),oc=1,Zb=U.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},db=U.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},mc=/([\:\-\_]+(.))/g,nc=/^moz([A-Z])/,ua=Q.prototype={ready:function(b){function a(){c||(c=!0,b())} -var c=!1;this.bind("DOMContentLoaded",a);Q(U).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+b])},length:0,push:Ra,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[E(b)]=b});var zb={};m("input,select,option,textarea,button,form".split(","),function(b){zb[la(b)]=!0});m({data:ub,inheritedData:Da,scope:function(b){return Da(b, -"$scope")},controller:xb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=rb(a);if(v(c))b.style[a]=c;else{var d;aa<=8&&(d=b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];aa<=8&&(d=d===""?p:d);return d}},attr:function(b,a,c){var d=E(a);if(Ea[d])if(v(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||D).specified?d:p;else if(v(c))b.setAttribute(a, -c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?p:b},prop:function(b,a,c){if(v(c))b[a]=c;else return b[a]},text:x(aa<9?function(b,a){if(b.nodeType==1){if(t(a))return b.innerText;b.innerText=a}else{if(t(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(t(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(t(a))return b.value;b.value=a},html:function(b,a){if(t(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a, -c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Lc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},hb={},Yc=U.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw new B("This browser does not support XMLHttpRequest."); -};Pb.$inject=["$provide"];Qb.$inject=["$locale"];Sb.$inject=["$locale"];var Vb=".",hd={yyyy:P("FullYear",4),yy:P("FullYear",2,0,!0),y:P("FullYear",1),MMMM:La("Month"),MMM:La("Month",!0),MM:P("Month",2,1),M:P("Month",1,1),dd:P("Date",2),d:P("Date",1),HH:P("Hours",2),H:P("Hours",1),hh:P("Hours",2,-12),h:P("Hours",1,-12),mm:P("Minutes",2),m:P("Minutes",1),ss:P("Seconds",2),s:P("Seconds",1),EEEE:La("Day"),EEE:La("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=a.getTimezoneOffset(); -return ib(a/60,2)+ib(Math.abs(a%60),2)}},gd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,fd=/^\d+$/;Rb.$inject=["$locale"];var dd=I(E),ed=I(la);Tb.$inject=["$parse"];var jd=I({restrict:"E",compile:function(a,c){c.href||c.$set("href","");return function(a,c){c.bind("click",function(a){if(!c.attr("href"))return a.preventDefault(),!1})}}}),kb={};m(Ea,function(a,c){var d=fa("ng-"+c);kb[d]=function(){return{priority:100,compile:function(){return function(a,g,i){a.$watch(i[d], -function(a){i.$set(c,!!a)})}}}}});m(["src","href"],function(a){var c=fa("ng-"+a);kb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){c&&(g.$set(a,c),aa&&e.prop(a,c))})}}}});var Oa={$addControl:D,$removeControl:D,$setValidity:D,$setDirty:D};Wb.$inject=["$element","$attrs","$scope"];var Ra=function(a){return["$timeout",function(c){var d={name:"form",restrict:"E",controller:Wb,compile:function(){return{pre:function(a,d,i,f){if(!i.action){var h=function(a){a.preventDefault? -a.preventDefault():a.returnValue=!1};Zb(d[0],"submit",h);d.bind("$destroy",function(){c(function(){db(d[0],"submit",h)},0,!1)})}var k=d.parent().controller("form"),j=i.name||i.ngForm;j&&(a[j]=f);k&&d.bind("$destroy",function(){k.$removeControl(f);j&&(a[j]=p);x(f,Oa)})}}}};return a?x(V(d),{restrict:"EAC"}):d}]},kd=Ra(),ld=Ra(!0),md=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,nd=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,od=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/, -ac={text:Qa,number:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);e.$parsers.push(function(a){var c=T(a);return c||od.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number",!1),p)});e.$formatters.push(function(a){return T(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!T(a)&&ah?(e.$setValidity("max", -!1),p):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return T(a)||wa(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1),p)})},url:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);a=function(a){return T(a)||md.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);a=function(a){return T(a)||nd.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email", -!1),p)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){t(d.name)&&c.attr("name",xa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,i=d.ngFalseValue;F(g)||(g=!0);F(i)||(i=!1);c.bind("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a=== -g});e.$parsers.push(function(a){return a?g:i})},hidden:D,button:D,submit:D,reset:D},bc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,g,i){i&&(ac[E(g.type)]||ac.text)(d,e,g,i,c,a)}}}],Na="ng-valid",Ma="ng-invalid",Pa="ng-pristine",Xb="ng-dirty",pd=["$scope","$exceptionHandler","$attrs","$element","$parse",function(a,c,d,e,g){function i(a,c){c=c?"-"+$a(c,"-"):"";e.removeClass((a?Ma:Na)+c).addClass((a?Na:Ma)+c)}this.$modelValue=this.$viewValue=Number.NaN; -this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var f=g(d.ngModel),h=f.assign;if(!h)throw B(Db+d.ngModel+" ("+pa(e)+")");this.$render=D;var k=e.inheritedData("$formController")||Oa,j=0,l=this.$error={};e.addClass(Pa);i(!0);this.$setValidity=function(a,c){if(l[a]!==!c){if(c){if(l[a]&&j--,!j)i(!0),this.$valid=!0,this.$invalid=!1}else i(!1),this.$invalid=!0,this.$valid=!1,j++;l[a]=!c;i(c,a);k.$setValidity(a, -c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Pa).addClass(Xb),k.$setDirty();m(this.$parsers,function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,h(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var o=this;a.$watch(function(){var c=f(a);if(o.$modelValue!==c){var d=o.$formatters,e=d.length;for(o.$modelValue=c;e--;)c=d[e](c);if(o.$viewValue!==c)o.$viewValue=c,o.$render()}})}],qd=function(){return{require:["ngModel", -"^?form"],controller:pd,link:function(a,c,d,e){var g=e[0],i=e[1]||Oa;i.$addControl(g);c.bind("$destroy",function(){i.$removeControl(g)})}}},rd=I({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),cc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var g=function(a){if(d.required&&(T(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g); -d.$observe("required",function(){g(e.$viewValue)})}}}},sd=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(R(a))});return c});e.$formatters.push(function(a){return J(a)?a.join(", "):p})}}},td=/^(true|false|\d+)$/,ud=function(){return{priority:100,compile:function(a,c){return td.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a, -c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},vd=S(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==p?"":a)})}),wd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],xd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe, -function(a){c.html(a||"")})}}],yd=jb("",!0),zd=jb("Odd",0),Ad=jb("Even",1),Bd=S({compile:function(a,c){c.$set("ngCloak",p);a.removeClass("ng-cloak")}}),Cd=[function(){return{scope:!0,controller:"@"}}],Dd=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],dc={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "),function(a){var c=fa("ng-"+a);dc[c]=["$parse",function(d){return function(e,g,i){var f=d(i[c]);g.bind(E(a),function(a){e.$apply(function(){f(e, -{$event:a})})})}}]});var Ed=S(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Fd=["$http","$templateCache","$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,i){var f=i.ngInclude||i.src,h=i.onload||"",k=i.autoscroll;return function(g,i){var o=0,m,n=function(){m&&(m.$destroy(),m=null);i.html("")};g.$watch(f,function(f){var p=++o;f?a.get(f,{cache:c}).success(function(a){p===o&&(m&&m.$destroy(),m=g.$new(),i.html(a),e(i.contents())(m), -v(k)&&(!k||g.$eval(k))&&d(),m.$emit("$includeContentLoaded"),g.$eval(h))}).error(function(){p===o&&n()}):n()})}}}}],Gd=S({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Hd=S({terminal:!0,priority:1E3}),Id=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,i){var f=i.count,h=g.attr(i.$attr.when),k=i.offset||0,j=e.$eval(h),l={},o=c.startSymbol(),r=c.endSymbol();m(j,function(a,e){l[e]=c(a.replace(d,o+f+"-"+k+r))});e.$watch(function(){var c= -parseFloat(e.$eval(f));return isNaN(c)?"":(j[c]||(c=a.pluralCat(c-k)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Jd=S({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a,c,i){var f=i.ngRepeat,i=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),h,k,j;if(!i)throw B("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f=i[1];h=i[2];i=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!i)throw B("'item' in 'item in collection' should be identifier or (key, value) but got '"+ -f+"'.");k=i[3]||i[1];j=i[2];var l=new eb;a.$watch(function(a){var e,f,i=a.$eval(h),m=gc(i,!0),p,u=new eb,C,A,v,t,y=c;if(J(i))v=i||[];else{v=[];for(C in i)i.hasOwnProperty(C)&&C.charAt(0)!="$"&&v.push(C);v.sort()}e=0;for(f=v.length;ex;)t.pop().element.remove()}for(;v.length>w;)v.pop()[0].element.remove()}var i;if(!(i=w.match(d)))throw B("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+w+"'.");var j=c(i[2]||i[1]),k=i[4]|| -i[6],l=i[5],m=c(i[3]||""),o=c(i[2]?i[1]:k),r=c(i[7]),v=[[{element:f,label:""}]];q&&(a(q)(e),q.removeClass("ng-scope"),q.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a,c=r(e)||[],d={},h,i,j,m,q,s;if(n){i=[];m=0;for(s=v.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 9bd31401e7..f75e6ed6fe 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -28,7 +28,7 @@ describe "AdminOrderMgmtCtrl", -> spyOn(returnedOrderCycles, "unshift") scope.initialise "api_key" httpBackend.flush() - expect(scope.suppliers).toEqual ["list of suppliers"] + #expect(scope.suppliers).toEqual ["list of suppliers"] expect(scope.distributors).toEqual ["list of distributors"] expect(scope.orderCycles).toEqual [ "oc1", "oc2", "oc3" ] expect(scope.initialiseVariables.calls.length).toEqual 1 @@ -608,4 +608,4 @@ describe "Auxiliary functions", -> date.setHours(5) date.setMinutes(10) date.setSeconds(30) - expect(formatDate(date)).toEqual "2010-06-15 05:10:30" \ No newline at end of file + expect(formatDate(date)).toEqual "2010-06-15 05:10:30" diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 4817ca0edd..172b2cdb1a 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -401,10 +401,12 @@ describe 'OrderCycle services', -> it 'loads enterprise fees', -> enterprise_fees = EnterpriseFee.index() $httpBackend.flush() - expect(enterprise_fees).toEqual [ + expected_fees = [ new EnterpriseFee.EnterpriseFee({id: 1, name: "Yayfee", enterprise_id: 1}) new EnterpriseFee.EnterpriseFee({id: 2, name: "FeeTwo", enterprise_id: 2}) ] + for fee, i in enterprise_fees + expect(fee.id).toEqual(expected_fees[i].id) it 'reports its loadedness', -> expect(EnterpriseFee.loaded).toBe(false) From 1d544aa0037c4014a525a0a10018397d089d7257 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 14:11:28 +1000 Subject: [PATCH 26/30] Fixing a couple of spec bugs --- app/assets/javascripts/darkswarm/services/order.js.coffee | 4 ++-- app/assets/javascripts/store/all.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 10c2d5322f..94e8e8d299 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -11,10 +11,10 @@ Darkswarm.factory 'Order', ($resource, Product, order)-> @shipping_methods[@shipping_method_id] requireShipAddress: -> - @shippingMethod().require_ship_address + @shippingMethod()?.require_ship_address shippingPrice: -> - @shippingMethod().price + @shippingMethod()?.price paymentMethod: -> @payment_methods[@payment_method_id] diff --git a/app/assets/javascripts/store/all.js b/app/assets/javascripts/store/all.js index 4da67e3567..59af65daa5 100644 --- a/app/assets/javascripts/store/all.js +++ b/app/assets/javascripts/store/all.js @@ -9,7 +9,7 @@ //= require store/spree_core //= require store/spree_auth //= require store/spree_promo -//= require shared/angular -//= require shared/angular-resource +//= require angular +//= require angular-resource //= require_tree . From 806f3348c0efa9cd1c0776f07298136e8cd1c10c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 14:34:50 +1000 Subject: [PATCH 27/30] Starting to rework checkout Login links --- ...thentication_actions_controller.js.coffee} | 2 +- .../controllers/checkout_controller.js.coffee | 2 +- app/views/shared/_menu.html.haml | 2 +- .../shop/checkout/_authentication.html.haml | 19 +++++++++++-------- .../checkout_controller_spec.js.coffee | 7 +++++-- 5 files changed, 19 insertions(+), 13 deletions(-) rename app/assets/javascripts/darkswarm/controllers/{menu_controller.js.coffee => authentication_actions_controller.js.coffee} (69%) diff --git a/app/assets/javascripts/darkswarm/controllers/menu_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee similarity index 69% rename from app/assets/javascripts/darkswarm/controllers/menu_controller.js.coffee rename to app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee index 889c8fa8f4..77fd2c2464 100644 --- a/app/assets/javascripts/darkswarm/controllers/menu_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee @@ -1,4 +1,4 @@ -window.MenuCtrl = Darkswarm.controller "MenuCtrl", ($scope, Navigation) -> +window.AuthenticationActionsCtrl = Darkswarm.controller "AuthenticationActionsCtrl", ($scope, Navigation) -> $scope.toggleLogin = -> Navigation.navigate "/login" diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 538255f74c..cd95a7faa3 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -2,7 +2,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, Order, storage) -> $scope.order = $scope.Order = Order # Binding accordion panel states to local storage - storage.bind $scope, "user" + storage.bind $scope, "user", { defaultValue: true } storage.bind $scope, "details" storage.bind $scope, "billing" storage.bind $scope, "shipping" diff --git a/app/views/shared/_menu.html.haml b/app/views/shared/_menu.html.haml index 3ece58cf4d..f5aee8cc17 100644 --- a/app/views/shared/_menu.html.haml +++ b/app/views/shared/_menu.html.haml @@ -1,6 +1,6 @@ %nav.top-bar %section.top-bar-section - %ul.left{"ng-controller" => "MenuCtrl"} + %ul.left{"ng-controller" => "AuthenticationActionsCtrl"} %li %a.icon{"ng-click" => "toggle()"} %i.fi-list diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 20a160aaed..282a713015 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,9 +1,12 @@ %fieldset - %accordion-group{heading: "User"} - .row - %section#checkout_login - .large-6.columns - = render partial: "shop/checkout/login" - %section#checkout_signup - .large-6.columns - = render partial: "shop/checkout/signup" + %accordion-group{heading: "User", "is-open" => "user"} + .row{"ng-controller" => "AuthenticationActionsCtrl"} + .large-12.columns + %button{"ng-click" => "toggle('/login')"} Login + %button{"ng-click" => "toggle('/signup')"} Signup + -#%section#checkout_login + -#.large-6.columns + -#= render partial: "shop/checkout/login" + -#%section#checkout_signup + -#.large-6.columns + -#= render partial: "shop/checkout/signup" diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee index 08358a4368..9730989427 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -6,6 +6,9 @@ describe "CheckoutCtrl", -> beforeEach -> module("Darkswarm") order = {} - inject ($controller) -> - scope = {} + inject ($controller, $rootScope) -> + scope = $rootScope.$new() ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: order} + + it "defaults the user accordion to visible", -> + expect(scope.user).toEqual true From 0b28e18225fe63b37f20f35536440a8c3ee5f015 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 15:05:39 +1000 Subject: [PATCH 28/30] Patching local storage; authentication actions --- ...uthentication_actions_controller.js.coffee | 2 +- .../controllers/checkout_controller.js.coffee | 9 +++++--- .../shared/angular-local-storage.js | 9 ++++---- .../shop/checkout/_authentication.html.haml | 13 +++++++---- app/views/shop/checkout/_form.html.haml | 22 +++++++++---------- app/views/shop/checkout/_login.html.haml | 14 ------------ app/views/shop/checkout/_signup.html.haml | 14 ------------ app/views/shop/checkout/_summary.html.haml | 2 +- app/views/shop/checkout/edit.html.haml | 2 +- .../checkout_controller_spec.js.coffee | 2 +- 10 files changed, 34 insertions(+), 55 deletions(-) delete mode 100644 app/views/shop/checkout/_login.html.haml delete mode 100644 app/views/shop/checkout/_signup.html.haml diff --git a/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee index 77fd2c2464..0fa5c6b05a 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee @@ -1,4 +1,4 @@ -window.AuthenticationActionsCtrl = Darkswarm.controller "AuthenticationActionsCtrl", ($scope, Navigation) -> +window.AuthenticationActionsCtrl = Darkswarm.controller "AuthenticationActionsCtrl", ($scope, Navigation, storage) -> $scope.toggleLogin = -> Navigation.navigate "/login" diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index cd95a7faa3..db944ab955 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,13 +1,16 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, Order, storage) -> +Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> $scope.order = $scope.Order = Order - # Binding accordion panel states to local storage - storage.bind $scope, "user", { defaultValue: true } + storage.bind $scope, "user", { defaultValue: true} + $scope.disable = -> + $scope.user = false + storage.bind $scope, "details" storage.bind $scope, "billing" storage.bind $scope, "shipping" storage.bind $scope, "payment" + $scope.purchase = (event)-> event.preventDefault() checkout.submit() diff --git a/app/assets/javascripts/shared/angular-local-storage.js b/app/assets/javascripts/shared/angular-local-storage.js index dfe1e2471c..2ac18eba9e 100644 --- a/app/assets/javascripts/shared/angular-local-storage.js +++ b/app/assets/javascripts/shared/angular-local-storage.js @@ -127,11 +127,12 @@ // If a value doesn't already exist store it as is if (!publicMethods.get(storeName)) { - publicMethods.set(storeName, opts.defaultValue); - } + publicMethods.set(storeName, $parse(key)($scope) || opts.defaultValue); + } else { + // If it does exist assign it to the $scope value + $parse(key).assign($scope, publicMethods.get(storeName)); + } - // If it does exist assign it to the $scope value - $parse(key).assign($scope, publicMethods.get(storeName)); // Register a listener for changes on the $scope value // to update the localStorage value diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 282a713015..5c0385d6ce 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,12 +1,17 @@ -%fieldset +%fieldset{"ng-controller" => "CheckoutCtrl"} %accordion-group{heading: "User", "is-open" => "user"} - .row{"ng-controller" => "AuthenticationActionsCtrl"} + .row .large-12.columns - %button{"ng-click" => "toggle('/login')"} Login - %button{"ng-click" => "toggle('/signup')"} Signup + .div{"ng-controller" => "AuthenticationActionsCtrl"} + %button{"ng-click" => "toggle('/login')"} Login + %button{"ng-click" => "toggle('/signup')"} Signup + %button{"ng-click" => "disable()"} Checkout as guest + + -#%section#checkout_login -#.large-6.columns -#= render partial: "shop/checkout/login" -#%section#checkout_signup -#.large-6.columns -#= render partial: "shop/checkout/signup" + diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index 759baabd64..66bea3b4cf 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -1,15 +1,13 @@ -%checkout{"ng-controller" => "CheckoutCtrl"} += f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form"} do |f| - = f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form"} do |f| + :javascript + angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) - :javascript - angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) + -#%pre + -#{{ order | json }} - -#%pre - -#{{ order | json }} - - .large-12.columns - = render partial: "shop/checkout/details", locals: {f: f} - = render partial: "shop/checkout/billing", locals: {f: f} - = render partial: "shop/checkout/shipping", locals: {f: f} - = render partial: "shop/checkout/payment", locals: {f: f} + .large-12.columns + = render partial: "shop/checkout/details", locals: {f: f} + = render partial: "shop/checkout/billing", locals: {f: f} + = render partial: "shop/checkout/shipping", locals: {f: f} + = render partial: "shop/checkout/payment", locals: {f: f} diff --git a/app/views/shop/checkout/_login.html.haml b/app/views/shop/checkout/_login.html.haml deleted file mode 100644 index 33bf41fed5..0000000000 --- a/app/views/shop/checkout/_login.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= form_for Spree::User.new, :html => {'data-type' => :json}, :as => :spree_user, :url => spree.spree_user_session_path do |f| - %fieldset - %legend I have an OFN Account - %p - = f.label :email, t(:email) - = f.email_field :email, :class => 'title', :tabindex => 1, :id => "login_spree_user_email" - %p - = f.label :password, t(:password) - = f.password_field :password, :class => 'title', :tabindex => 2, :id => "login_spree_user_password" - %p - %label - = f.check_box :remember_me - = f.label :remember_me, t(:remember_me) - %p= f.submit t(:login), :class => 'button primary', :tabindex => 3, :id => "login_spree_user_remember_me" diff --git a/app/views/shop/checkout/_signup.html.haml b/app/views/shop/checkout/_signup.html.haml deleted file mode 100644 index 81ed7744d0..0000000000 --- a/app/views/shop/checkout/_signup.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= form_for Spree::User.new, :as => :spree_user, :url => spree.spree_user_registration_path(@spree_user) do |f| - %fieldset - %legend New to OFN? - %p - = f.label :email, t(:email) - = f.email_field :email, :class => 'title', :id => "signup_spree_user_email" - %p - = f.label :password, t(:password) - = f.password_field :password, :class => 'title', :id => "signup_spree_user_password" - %p - = f.label :password_confirmation, t(:confirm_password) - = f.password_field :password_confirmation, :class => 'title', :id => "signup_spree_user_password_confirmation" - - = f.submit "Sign Up", :class => 'button' diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index a852e666e1..77ec80b1e6 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -1,4 +1,4 @@ -%orderdetails{"ng-controller" => "CheckoutCtrl"} +%orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| %fieldset %legend Your Order diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 09b2561409..03f76703bb 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -6,7 +6,7 @@ = render partial: "shop/details" - %checkout + %checkout{"ng-controller" => "CheckoutCtrl"} %accordion.row{"close-others" => "false"} .large-9.columns - unless spree_current_user diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee index 9730989427..ab2369dedb 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -11,4 +11,4 @@ describe "CheckoutCtrl", -> ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: order} it "defaults the user accordion to visible", -> - expect(scope.user).toEqual true + expect(scope.userpanel).toEqual true From 9f60f6dab3a8925150130010c7736d75c2fa41d9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 15:10:30 +1000 Subject: [PATCH 29/30] Fixing a scoping issue --- .../darkswarm/controllers/checkout_controller.js.coffee | 2 ++ app/views/shop/checkout/_authentication.html.haml | 5 ++--- app/views/shop/checkout/edit.html.haml | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index db944ab955..aa34740744 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -4,6 +4,8 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "user", { defaultValue: true} $scope.disable = -> $scope.user = false + $scope.details = true + console.log $scope.details storage.bind $scope, "details" storage.bind $scope, "billing" diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 5c0385d6ce..2b81b9383e 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,13 +1,12 @@ -%fieldset{"ng-controller" => "CheckoutCtrl"} +%fieldset %accordion-group{heading: "User", "is-open" => "user"} .row .large-12.columns - .div{"ng-controller" => "AuthenticationActionsCtrl"} + .span{"ng-controller" => "AuthenticationActionsCtrl"} %button{"ng-click" => "toggle('/login')"} Login %button{"ng-click" => "toggle('/signup')"} Signup %button{"ng-click" => "disable()"} Checkout as guest - -#%section#checkout_login -#.large-6.columns -#= render partial: "shop/checkout/login" diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 03f76703bb..2fb957c46a 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -6,8 +6,8 @@ = render partial: "shop/details" - %checkout{"ng-controller" => "CheckoutCtrl"} - %accordion.row{"close-others" => "false"} + %accordion.row{"close-others" => "false"} + %checkout{"ng-controller" => "CheckoutCtrl"} .large-9.columns - unless spree_current_user = render partial: "shop/checkout/authentication" From d2c5c6dcaf5a5ad00e890b6a1ca90dbd702e358c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 17:52:54 +1000 Subject: [PATCH 30/30] Starting to add inline validation and behaviour to checkout --- .../controllers/checkout_controller.js.coffee | 43 ++++++++++++++++++- .../shop/checkout/_authentication.html.haml | 17 +++----- app/views/shop/checkout/_details.html.haml | 43 ++++++++++++------- app/views/shop/checkout/_form.html.haml | 6 ++- app/views/shop/checkout/_summary.html.haml | 2 +- app/views/shop/checkout/edit.html.haml | 3 +- .../checkout_controller_spec.js.coffee | 2 +- 7 files changed, 82 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index aa34740744..6ebd9d1367 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,18 +1,59 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> + window.tmp = $scope $scope.order = $scope.Order = Order storage.bind $scope, "user", { defaultValue: true} $scope.disable = -> $scope.user = false $scope.details = true - console.log $scope.details storage.bind $scope, "details" storage.bind $scope, "billing" storage.bind $scope, "shipping" storage.bind $scope, "payment" + # Validation utilities to keep things DRY + $scope.dirtyValid = (name)-> + $scope.dirty(name) and $scope.valid(name) + $scope.dirty = (name)-> + $scope.checkout[name].$dirty + $scope.valid = (name)-> + $scope.checkout[name].$invalid + $scope.error = (name)-> + $scope.checkout[name].$error + $scope.required = (name)-> + $scope.error(name).required + $scope.email = (name)-> + $scope.error(name).email + $scope.number = (name)-> + $scope.error(name).number + +Darkswarm.controller "DetailsSubCtrl", ($scope) -> + $scope.detailsValid = -> + $scope.detailsFields().every (field)-> + $scope.checkout[field].$valid + + $scope.detailsFields = -> + ["order[email]", + "order[bill_address_attributes][phone]", + "order[bill_address_attributes][firstname]", + "order[bill_address_attributes][lastname]"] + + $scope.emailName = 'order[email]' + $scope.emailValid = -> + $scope.dirtyValid($scope.emailName) + $scope.emailError = -> + return "can't be blank" if $scope.required($scope.emailName) + return "must be valid" if $scope.email($scope.emailName) + + $scope.phoneName = "order[bill_address_attributes][phone]" + $scope.phoneValid = -> + $scope.dirtyValid($scope.phoneName) + $scope.phoneError = -> + "must be a number" $scope.purchase = (event)-> event.preventDefault() checkout.submit() + + diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 2b81b9383e..ac87ce8e6f 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,16 +1,9 @@ %fieldset %accordion-group{heading: "User", "is-open" => "user"} .row - .large-12.columns - .span{"ng-controller" => "AuthenticationActionsCtrl"} - %button{"ng-click" => "toggle('/login')"} Login - %button{"ng-click" => "toggle('/signup')"} Signup + .large-4.columns.text-center{"ng-controller" => "AuthenticationActionsCtrl"} + %button{"ng-click" => "toggle('/login')"} Login + .large-4.columns.text-center{"ng-controller" => "AuthenticationActionsCtrl"} + %button{"ng-click" => "toggle('/signup')"} Signup + .large-4.columns.text-center %button{"ng-click" => "disable()"} Checkout as guest - - -#%section#checkout_login - -#.large-6.columns - -#= render partial: "shop/checkout/login" - -#%section#checkout_signup - -#.large-6.columns - -#= render partial: "shop/checkout/signup" - diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 20408d0e44..cb7c189ad2 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -1,21 +1,32 @@ %fieldset#details %accordion-group{"is-open" => "details"} - %accordion-heading + %div{"ng-controller" => "DetailsSubCtrl"} + {{ detailsValid() }} + %accordion-heading + .row + .large-6.columns + Customer Details + .large-6.columns.text-right + {{ order.bill_address.firstname }} + {{ order.bill_address.lastname }} .row .large-6.columns - Customer Details - .large-6.columns.text-right - {{ order.bill_address.firstname }} - {{ order.bill_address.lastname }} - .row - .large-6.columns - = f.text_field :email + = f.email_field :email, required: "", "ng-model" => "order.email", + "ng-class" => "{error: '!emailValid()'}" + %small.error.medium.input-text{"ng-show" => "emailValid()"} + {{ emailError() }} + + = f.fields_for :bill_address, @order.bill_address do |ba| + .large-6.columns + = ba.number_field :phone, "ng-model" => "order.bill_address.phone", required: "", + "ng-class" => "{error: '!phoneValid()'}" + %small.error.medium.input-text{"ng-show" => "phoneValid()"} + {{ phoneError() }} + = f.fields_for :bill_address, @order.bill_address do |ba| - .large-6.columns - = ba.text_field :phone, "ng-model" => "order.bill_address.phone" - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-6.columns - = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" - .large-6.columns - = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" + .row + .large-6.columns + = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" + .large-6.columns + = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" + diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index 66bea3b4cf..faf66d578f 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -1,4 +1,8 @@ -= f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form"} do |f| += f_form_for current_order, url: main_app.shop_update_checkout_path, + html: {name: "checkout", + id: "checkout_form", + novalidate: "", + name: "checkout"} do |f| :javascript angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 77ec80b1e6..730263a249 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -23,6 +23,6 @@ %th= label %td= total - = f.submit "Purchase", class: "button" + = f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid" %a.button.secondary{href: cart_url} Back to Cart diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 2fb957c46a..6d2a0feecc 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -15,6 +15,5 @@ = render partial: "shop/checkout/form" .large-3.columns - .row - = render partial: "shop/checkout/summary" + = render partial: "shop/checkout/summary" diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee index ab2369dedb..9730989427 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -11,4 +11,4 @@ describe "CheckoutCtrl", -> ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: order} it "defaults the user accordion to visible", -> - expect(scope.userpanel).toEqual true + expect(scope.user).toEqual true