From 42cbfdc04f72ee7d883e9d565f1bd7783d8ce1a8 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:01:53 +1100 Subject: [PATCH 01/75] Adding bindonce to product page --- .../javascripts/darkswarm/all.js.coffee | 1 + .../javascripts/darkswarm/darkswarm.js.coffee | 2 +- app/assets/javascripts/shared/bindonce.min.js | 1 + app/views/shop/shop/_products.html.haml | 21 +++++++++++++------ app/views/shop/shop/_variant.html.haml | 4 ++-- 5 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/shared/bindonce.min.js diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index ea8c47eed8..9619834664 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -6,6 +6,7 @@ #= require angular #= require angular-resource #= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT +#= require ../shared/bindonce.min.js # #= require ../shared/jquery.timeago #= require foundation diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index bffcc780e3..95ffd605ce 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) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'pasvaz.bindonce']).config ($httpProvider) -> $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/shared/bindonce.min.js b/app/assets/javascripts/shared/bindonce.min.js new file mode 100644 index 0000000000..0edd3d57d4 --- /dev/null +++ b/app/assets/javascripts/shared/bindonce.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};return l}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file diff --git a/app/views/shop/shop/_products.html.haml b/app/views/shop/shop/_products.html.haml index f8a2cbdbf7..ac9b21772c 100644 --- a/app/views/shop/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -18,15 +18,18 @@ %h3.text-center Loading Products %tbody{"ng-repeat" => "product in data.products | filter:query"} %tr{"class" => "product product-{{ product.id }}"} - %td.name - %img{"ng-src" => "{{ product.master.images[0].small_url }}"} + + %td.name{bindonce: ""} + %img{"bo-src" => "{{ product.master.images[0].small_url }}"} %div %h5 {{ product.name }} %a{"data-reveal-id" => "producer_details_{{product.supplier.id}}", "data-reveal" => ""} {{ product.supplier.name }} - %td.notes {{ product.notes | truncate:80 }} - %td + + %td.notes{bindonce: ""} {{ product.notes | truncate:80 }} + + %td{bindonce: ""} %span{"ng-hide" => "product.variants.length > 0"} {{ product.master.options_text }} %span{"ng-show" => "product.variants.length > 0"} %img.collapse{src: "/assets/collapse.png", @@ -45,6 +48,7 @@ name: "variants[{{product.master.id}}]", id: "variants_{{product.master.id}}", "ng-model" => "product.quantity"} + %td.group_buy %span{"ng-show" => "product.group_buy && (product.variants.length == 0)"} %input{type: :number, @@ -52,13 +56,18 @@ max: "{{product.on_demand && 9999 || product.count_on_hand }}", name: "variant_attributes[{{product.master.id}}][max_quantity]", "ng-model" => "product.max_quantity"} - %td.price.text-right + + %td.price.text-right{bindonce: ""} %small{"ng-show" => "(product.variants.length > 0)"} from {{ productPrice(product) | currency }} - %tr.product-description + + %tr.product-description{bindonce: ""} %td{colspan: 2}{{ product.notes | truncate:80 }} + %tr.variant{"ng-repeat" => "variant in product.variants", "ng-show" => "product.show_variants"} + = render partial: "shop/shop/variant" + %input.button.right{type: :submit, value: "Add to Cart"} diff --git a/app/views/shop/shop/_variant.html.haml b/app/views/shop/shop/_variant.html.haml index 3dade13410..fa9b00d5f3 100644 --- a/app/views/shop/shop/_variant.html.haml +++ b/app/views/shop/shop/_variant.html.haml @@ -1,7 +1,7 @@ %td %td.notes -%td {{variant.options_text}} +%td{bindonce: ""} {{variant.options_text}} %td %input{type: :number, value: nil, @@ -16,5 +16,5 @@ max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variant_attributes[{{variant.id}}][max_quantity]", "ng-model" => "variant.max_quantity"} -%td.price.text-right +%td.price.text-right{bindonce: ""} {{ variant.price | currency }} From a040c150cfcfeed63348a9a9f5b6d031e120c673 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:12:16 +1100 Subject: [PATCH 02/75] 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 f3f676ce266888689fc81fa5a1c108d6a2fed938 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:29:35 +1100 Subject: [PATCH 03/75] 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 a4fcabe11f28d6c3cd002eb2d97bb972461f744e Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:44:36 +1100 Subject: [PATCH 04/75] 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 bf6eb4ace15c3688761dc73206502ef0fd66dcc6 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:53:37 +1100 Subject: [PATCH 05/75] 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 a73d1740098f5e7f2aef596a45bc3f02ab6242e7 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:01:36 +1100 Subject: [PATCH 06/75] 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 74d96a5a07424246b35fe41447377a0307b77cf7 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:02:25 +1100 Subject: [PATCH 07/75] 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 b9e8d5ce67a4eb45e851f02e14ff2da54d212b2f Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 14:11:48 +1100 Subject: [PATCH 08/75] 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 79322fbab442204ded70a0a227020a57d841fa17 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 16:58:50 +1100 Subject: [PATCH 09/75] 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 a8a2e36375be16dfad1772ea6a938ccb07bcd865 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 17:13:43 +1100 Subject: [PATCH 10/75] 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 3574c9179078c46e5641c3532f6b95756a291bc5 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 13:08:00 +1100 Subject: [PATCH 11/75] Patching a bug with bo-src --- app/views/shop/shop/_products.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shop/shop/_products.html.haml b/app/views/shop/shop/_products.html.haml index ac9b21772c..0221910607 100644 --- a/app/views/shop/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -19,8 +19,8 @@ %tbody{"ng-repeat" => "product in data.products | filter:query"} %tr{"class" => "product product-{{ product.id }}"} - %td.name{bindonce: ""} - %img{"bo-src" => "{{ product.master.images[0].small_url }}"} + %td.name{bindonce: "product"} + %img{"bo-src" => "product.master.images[0].small_url"} %div %h5 {{ product.name }} From 017ed1b9375a01f9a563a37ba3822559a26cc5d9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 1 Apr 2014 17:39:27 +1100 Subject: [PATCH 12/75] Adding an infinite scroll filter to reduce page load times, hopefully --- app/assets/javascripts/darkswarm/all.js.coffee | 1 + .../controllers/products_controller.js.coffee | 5 +++++ app/assets/javascripts/darkswarm/darkswarm.js.coffee | 2 +- .../javascripts/shared/ng-infinite-scroll.min.js | 2 ++ app/views/shop/shop/_products.html.haml | 11 +++++++---- 5 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/shared/ng-infinite-scroll.min.js diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 9619834664..59aafb8946 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -7,6 +7,7 @@ #= require angular-resource #= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT #= require ../shared/bindonce.min.js +#= require ../shared/ng-infinite-scroll.min.js # #= require ../shared/jquery.timeago #= require foundation diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 34ec81c5d2..9ba11baa5b 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,8 +1,13 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) -> $scope.data = Product.data + $scope.limit = 3 $scope.order_cycle = OrderCycle.order_cycle Product.update() + $scope.incrementLimit = -> + if $scope.limit < $scope.data.products.length + $scope.limit = $scope.limit + 1 + $scope.searchKeypress = (e)-> code = e.keyCode || e.which if code == 13 diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 95ffd605ce..909bfbfd55 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', 'pasvaz.bindonce']).config ($httpProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider) -> $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/shared/ng-infinite-scroll.min.js b/app/assets/javascripts/shared/ng-infinite-scroll.min.js new file mode 100644 index 0000000000..d4410b93c0 --- /dev/null +++ b/app/assets/javascripts/shared/ng-infinite-scroll.min.js @@ -0,0 +1,2 @@ +/* ng-infinite-scroll - v1.0.0 - 2013-02-23 */ +var mod;mod=angular.module("infinite-scroll",[]),mod.directive("infiniteScroll",["$rootScope","$window","$timeout",function(i,n,e){return{link:function(t,l,o){var r,c,f,a;return n=angular.element(n),f=0,null!=o.infiniteScrollDistance&&t.$watch(o.infiniteScrollDistance,function(i){return f=parseInt(i,10)}),a=!0,r=!1,null!=o.infiniteScrollDisabled&&t.$watch(o.infiniteScrollDisabled,function(i){return a=!i,a&&r?(r=!1,c()):void 0}),c=function(){var e,c,u,d;return d=n.height()+n.scrollTop(),e=l.offset().top+l.height(),c=e-d,u=n.height()*f>=c,u&&a?i.$$phase?t.$eval(o.infiniteScroll):t.$apply(o.infiniteScroll):u?r=!0:void 0},n.on("scroll",c),t.$on("$destroy",function(){return n.off("scroll",c)}),e(function(){return o.infiniteScrollImmediateCheck?t.$eval(o.infiniteScrollImmediateCheck)?c():void 0:c()},0)}}}]); \ No newline at end of file diff --git a/app/views/shop/shop/_products.html.haml b/app/views/shop/shop/_products.html.haml index 0221910607..833b338cfa 100644 --- a/app/views/shop/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -1,9 +1,13 @@ -%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null"} +%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", +"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} + = form_for :order, :url => populate_orders_path, html: {:class => "custom"} do %input#search.text{"ng-model" => "query", placeholder: "Search", "ng-keypress" => "searchKeypress($event)"} %input.button.right{type: :submit, value: "Add to Cart"} + {{ limit }} + %table %thead %th.name Item @@ -16,7 +20,7 @@ %tr %td{colspan: 6} %h3.text-center Loading Products - %tbody{"ng-repeat" => "product in data.products | filter:query"} + %tbody{"ng-repeat" => "product in data.products | filter:query | limitTo: limit track by product.id"} %tr{"class" => "product product-{{ product.id }}"} %td.name{bindonce: "product"} @@ -64,8 +68,7 @@ %tr.product-description{bindonce: ""} %td{colspan: 2}{{ product.notes | truncate:80 }} - %tr.variant{"ng-repeat" => "variant in product.variants", "ng-show" => "product.show_variants"} - + %tr.variant{"ng-repeat" => "variant in product.variants", "ng-if" => "product.show_variants"} = render partial: "shop/shop/variant" %input.button.right{type: :submit, value: "Add to Cart"} From 577c91aca5500d67ffe34e51ada03bbf333cf29c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 16:15:19 +1000 Subject: [PATCH 13/75] 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 14/75] 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 15/75] 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 16/75] 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 17/75] 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 18/75] 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 19/75] 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 20/75] 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 21/75] 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 22/75] 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 From 566b58f3cba0e70cb522c3d0a1cb187fe26438ad Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 28 Mar 2014 17:25:52 +1100 Subject: [PATCH 23/75] 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 645c422d9efdf4031ec586694620434bb245d2f2 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 10:35:16 +1100 Subject: [PATCH 24/75] 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 9687e8867e4ce12f092666179e3e76f287e8fe33 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 11:38:59 +1100 Subject: [PATCH 25/75] 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 298055bdbba1a25167bdd5744ffc555227d09052 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Apr 2014 11:54:28 +1100 Subject: [PATCH 26/75] 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 330b2100cc54c181c49cac2448ac646059e89fc1 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 4 Apr 2014 12:36:12 +1300 Subject: [PATCH 27/75] 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 b6c746f5b82e3b44733aedf41716e8802e72ae99 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 4 Apr 2014 12:36:33 +1300 Subject: [PATCH 28/75] 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 23597997b446799d9c18e6b091e17a9f6a31bbd3 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 13:14:32 +1000 Subject: [PATCH 29/75] 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 908e7993261084351260d2d0d06d3809810c6024 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:10:22 +1000 Subject: [PATCH 30/75] 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 909bfbfd55..efb984114e 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', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'pasvaz.bindonce', 'infinite-scroll']).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 a1eae915c8d9b2ed8ac7e986676eb6b7c42a21f9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:29:42 +1000 Subject: [PATCH 31/75] 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 efb984114e..afb668766f 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 a73b714d9e8164fb8894e9e4f25387941fb1c4b9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:29:51 +1000 Subject: [PATCH 32/75] 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 8a45b6c012e7276f9da43ab8d1595b6338561936 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 14:46:48 +1000 Subject: [PATCH 33/75] 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 0f1f8975e14baca6eb4999a73f20c3aa414099f9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 16:15:19 +1000 Subject: [PATCH 34/75] 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 b99fe20b7c447debf58e2fedf1a2aca82963c21c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Mon, 7 Apr 2014 18:23:37 +1000 Subject: [PATCH 35/75] 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 195f62dba1b65e5880f9002fc04377f701cf8332 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 12:58:37 +1000 Subject: [PATCH 36/75] 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 59aafb8946..6fffd3563a 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -4,10 +4,12 @@ #= require spin # #= require angular +#= require angular-cookies #= require angular-resource #= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js +#= 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 afb668766f..bbf35bfd9a 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', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider, $tooltipProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll']).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 5ac5b74f0195f56dfab8d4fbb6a30a629e69262b Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 13:25:35 +1000 Subject: [PATCH 37/75] 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 8084262efed67dd4780c7d65ecf3a2045c7cbe4d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 13:40:45 +1000 Subject: [PATCH 38/75] 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 f2157e0356e72a6685b0c123a58cfa30e62a1fe4 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 14:11:28 +1000 Subject: [PATCH 39/75] 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 a4cba99de6b931f4ade25d1d4d8bfc7bfa7ac23e Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 14:34:50 +1000 Subject: [PATCH 40/75] 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 8495c1306397323b291413ff735c126b2a32427c Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 15:05:39 +1000 Subject: [PATCH 41/75] 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 ffb21cfc49d1a4ec1a39ef359e704771b08f1cf2 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 15:10:30 +1000 Subject: [PATCH 42/75] 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 e641d2a7886fddd13ab795754b7d86bd3c6a27b3 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 17:52:54 +1000 Subject: [PATCH 43/75] 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 From 101a4a0bed2f7524e0c20c543688c8de85c714dd Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 18:11:07 +1000 Subject: [PATCH 44/75] Fixing a typo --- app/assets/javascripts/darkswarm/darkswarm.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index e60684a6fb..bbf35bfd9a 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,3 @@ -<<<<<<< HEAD window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' From 4bb97762336efee23bf02df17bb29619849c4cae Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 18:16:46 +1000 Subject: [PATCH 45/75] Adding bindonce and infinite scroll to karma --- config/ng-test.conf.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index e8a01b3c9c..7814873184 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -10,6 +10,8 @@ module.exports = function(config) { '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/shared/bindonce.min.js', + 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', 'app/assets/javascripts/admin/shared_directives.js.coffee', 'app/assets/javascripts/admin/shared_services.js.coffee', From b293efd110325f52bed3f1ab49d3946af3ee4623 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Tue, 8 Apr 2014 18:39:50 +1000 Subject: [PATCH 46/75] Fixing issue at https://github.com/angular/angular.js/wiki/Understanding-Scopes - hiding our attributes inside objects so child scopes can access them safely --- .../controllers/checkout_controller.js.coffee | 21 +++++++++++++++---- .../shop/checkout/_authentication.html.haml | 2 +- app/views/shop/checkout/_billing.html.haml | 2 +- app/views/shop/checkout/_details.html.haml | 4 +++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 6ebd9d1367..93273b7ed0 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,16 +1,18 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> window.tmp = $scope $scope.order = $scope.Order = Order + $scope.accordion = {} storage.bind $scope, "user", { defaultValue: true} $scope.disable = -> $scope.user = false $scope.details = true - storage.bind $scope, "details" - storage.bind $scope, "billing" - storage.bind $scope, "shipping" - storage.bind $scope, "payment" + + storage.bind $scope, "accordion.details" + storage.bind $scope, "accordion.billing" + storage.bind $scope, "accordion.shipping" + storage.bind $scope, "accordion.payment" # Validation utilities to keep things DRY $scope.dirtyValid = (name)-> @@ -28,10 +30,21 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> $scope.number = (name)-> $scope.error(name).number + +# READ THIS FIRST +# https://github.com/angular/angular.js/wiki/Understanding-Scopes + Darkswarm.controller "DetailsSubCtrl", ($scope) -> $scope.detailsValid = -> $scope.detailsFields().every (field)-> $scope.checkout[field].$valid + + $scope.$watch -> + $scope.detailsValid() + , (valid)-> + if valid + $scope.accordion.details = false + $scope.accordion.billing = true $scope.detailsFields = -> ["order[email]", diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index ac87ce8e6f..133cd69a09 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,5 +1,5 @@ %fieldset - %accordion-group{heading: "User", "is-open" => "user"} + %accordion-group{heading: "User", "is-open" => "accordion.user"} .row .large-4.columns.text-center{"ng-controller" => "AuthenticationActionsCtrl"} %button{"ng-click" => "toggle('/login')"} Login diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index ccf1d49091..1bbbc3fd39 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -1,5 +1,5 @@ %fieldset#billing - %accordion-group + %accordion-group{"is-open" => "accordion.billing"} %accordion-heading .row .large-6.columns diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index cb7c189ad2..ad8c5acbeb 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{"is-open" => "details"} + %accordion-group{"is-open" => "accordion.details"} %div{"ng-controller" => "DetailsSubCtrl"} {{ detailsValid() }} %accordion-heading @@ -12,6 +12,7 @@ .row .large-6.columns = f.email_field :email, required: "", "ng-model" => "order.email", + "ng-change" => "next()", "ng-class" => "{error: '!emailValid()'}" %small.error.medium.input-text{"ng-show" => "emailValid()"} {{ emailError() }} @@ -27,6 +28,7 @@ .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" From 09d22f74ecb0c78970ceee4d4b94d3915ddb3da0 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 9 Apr 2014 11:51:12 +1000 Subject: [PATCH 47/75] Renaming some methods in our experimental --- .../controllers/checkout_controller.js.coffee | 26 ++++++++++--------- .../shop/checkout/_authentication.html.haml | 2 +- app/views/shop/checkout/_details.html.haml | 4 +-- app/views/shop/checkout/edit.html.haml | 2 +- .../shop/checkout_controller_spec.rb | 2 ++ 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 93273b7ed0..28ade6e08f 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -3,24 +3,24 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> $scope.order = $scope.Order = Order $scope.accordion = {} - storage.bind $scope, "user", { defaultValue: true} - $scope.disable = -> - $scope.user = false - $scope.details = true - + $scope.show = (name)-> + $scope.accordion[name] = true + storage.bind $scope, "accordion.user", { defaultValue: true} storage.bind $scope, "accordion.details" storage.bind $scope, "accordion.billing" storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" # Validation utilities to keep things DRY - $scope.dirtyValid = (name)-> - $scope.dirty(name) and $scope.valid(name) + $scope.dirtyInvalid = (name)-> + $scope.dirty(name) and $scope.invalid(name) $scope.dirty = (name)-> $scope.checkout[name].$dirty - $scope.valid = (name)-> + $scope.invalid = (name)-> $scope.checkout[name].$invalid + + # Validations $scope.error = (name)-> $scope.checkout[name].$error $scope.required = (name)-> @@ -43,10 +43,12 @@ Darkswarm.controller "DetailsSubCtrl", ($scope) -> $scope.detailsValid() , (valid)-> if valid - $scope.accordion.details = false - $scope.accordion.billing = true + $scope.show("billing") $scope.detailsFields = -> + + {"order[email]" : {email: "must be email", required: "field required"}} + ["order[email]", "order[bill_address_attributes][phone]", "order[bill_address_attributes][firstname]", @@ -54,14 +56,14 @@ Darkswarm.controller "DetailsSubCtrl", ($scope) -> $scope.emailName = 'order[email]' $scope.emailValid = -> - $scope.dirtyValid($scope.emailName) + $scope.dirtyInvalid($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.dirtyInvalid($scope.phoneName) $scope.phoneError = -> "must be a number" diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 133cd69a09..891958de25 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -6,4 +6,4 @@ .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 + %button{"ng-click" => "show('details')"} Checkout as guest diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index ad8c5acbeb..ef5fb98d53 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -1,7 +1,6 @@ %fieldset#details %accordion-group{"is-open" => "accordion.details"} %div{"ng-controller" => "DetailsSubCtrl"} - {{ detailsValid() }} %accordion-heading .row .large-6.columns @@ -12,8 +11,8 @@ .row .large-6.columns = f.email_field :email, required: "", "ng-model" => "order.email", - "ng-change" => "next()", "ng-class" => "{error: '!emailValid()'}" + %small.error.medium.input-text{"ng-show" => "emailValid()"} {{ emailError() }} @@ -31,4 +30,3 @@ .large-6.columns = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" - diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 6d2a0feecc..17ded5cc12 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" - %accordion.row{"close-others" => "false"} + %accordion.row{"close-others" => "true"} %checkout{"ng-controller" => "CheckoutCtrl"} .large-9.columns - unless spree_current_user diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index ed91818fbd..5b06b7c66f 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -8,6 +8,7 @@ describe Shop::CheckoutController do order.stub(:checkout_allowed?).and_return true controller.stub(:check_authorization).and_return true end + it "redirects home when no distributor is selected" do get :edit response.should redirect_to root_path @@ -46,6 +47,7 @@ describe Shop::CheckoutController do end describe "building the order" do + before do controller.stub(:current_distributor).and_return(distributor) controller.stub(:current_order_cycle).and_return(order_cycle) From b5550c048ab8d58238fbf039caca2bf0c36b5d7b Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 9 Apr 2014 14:33:45 +1000 Subject: [PATCH 48/75] Starting on JSON checkout, moving Order properties to Order.order --- .../controllers/checkout_controller.js.coffee | 10 ++++--- .../javascripts/darkswarm/darkswarm.js.coffee | 1 + .../darkswarm/services/order.js.coffee | 27 +++++++++++++------ app/controllers/shop/checkout_controller.rb | 21 ++++++++++----- .../shop/checkout_controller_spec.rb | 17 +++++++++++- .../checkout_controller_spec.js.coffee | 14 ++++++++-- .../darkswarm/services/order_spec.js.coffee | 19 ++++++++----- 7 files changed, 82 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 28ade6e08f..b3af7a44eb 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,6 +1,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> window.tmp = $scope - $scope.order = $scope.Order = Order + $scope.Order = Order + $scope.order = Order.order $scope.accordion = {} $scope.show = (name)-> @@ -30,6 +31,10 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> $scope.number = (name)-> $scope.error(name).number + $scope.purchase = (event)-> + event.preventDefault() + $scope.Order.submit() + # READ THIS FIRST # https://github.com/angular/angular.js/wiki/Understanding-Scopes @@ -67,8 +72,5 @@ Darkswarm.controller "DetailsSubCtrl", ($scope) -> $scope.phoneError = -> "must be a number" - $scope.purchase = (event)-> - event.preventDefault() - checkout.submit() diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index bbf35bfd9a..ac9eefe0df 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,5 +1,6 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') + $httpProvider.defaults.headers.put['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 94e8e8d299..6310d875bb 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -1,14 +1,25 @@ -Darkswarm.factory 'Order', ($resource, Product, order)-> +Darkswarm.factory 'Order', ($resource, Product, order, $http)-> new class Order + errors: {} constructor: -> - @[name] = method for name, method of order # Clone all data from the order JSON object - + @order = order # Here we default to the first shipping method if none is selected - @shipping_method_id ||= parseInt(Object.keys(@shipping_methods)[0]) - @ship_address_same_as_billing ?= true + @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) + @order.ship_address_same_as_billing ?= true + + submit: -> + $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)-> + console.log "success" + console.log data + .error (data, status)-> + console.log "error" + console.log data + + preprocess: -> + @order shippingMethod: -> - @shipping_methods[@shipping_method_id] + @order.shipping_methods[@order.shipping_method_id] requireShipAddress: -> @shippingMethod()?.require_ship_address @@ -17,8 +28,8 @@ Darkswarm.factory 'Order', ($resource, Product, order)-> @shippingMethod()?.price paymentMethod: -> - @payment_methods[@payment_method_id] + @order.payment_methods[@order.payment_method_id] cartTotal: -> - @shippingPrice() + @display_total + @shippingPrice() + @order.display_total diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb index be97bd604f..2199d4be03 100644 --- a/app/controllers/shop/checkout_controller.rb +++ b/app/controllers/shop/checkout_controller.rb @@ -22,8 +22,7 @@ class Shop::CheckoutController < Spree::CheckoutController state_callback(:after) else flash[:error] = t(:payment_processing_failed) - clear_ship_address - render :edit + update_failed return end end @@ -32,16 +31,26 @@ class Shop::CheckoutController < Spree::CheckoutController flash.notice = t(:order_processed_successfully) respond_with(@order, :location => order_path(@order)) else - clear_ship_address - render :edit + update_failed end else - clear_ship_address - render :edit + update_failed end end private + + def update_failed + clear_ship_address + respond_to do |format| + format.html do + render :edit + end + format.js do + render json: @order.errors.to_json, status: 400 + end + end + end # When we have a pickup Shipping Method, we clone the distributor address into ship_address before_save # We don't want this data in the form, so we clear it out diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index 5b06b7c66f..c6d6851208 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -47,12 +47,12 @@ describe Shop::CheckoutController do end describe "building the order" do - before do controller.stub(:current_distributor).and_return(distributor) controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return(order) end + it "does not clone the ship address from distributor when shipping method requires address" do get :edit assigns[:order].ship_address.address1.should be_nil @@ -78,6 +78,21 @@ describe Shop::CheckoutController do end end + context "via xhr" do + before do + controller.stub(:current_distributor).and_return(distributor) + + controller.stub(:current_order_cycle).and_return(order_cycle) + controller.stub(:current_order).and_return(order) + end + + it "returns errors" do + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 400 + response.body.should == assigns[:order].errors.to_json + end + end + describe "Paypal routing" do let(:payment_method) { create(:payment_method, type: "Spree::BillingIntegration::PaypalExpress") } before do 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..c6efca67b1 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -5,10 +5,20 @@ describe "CheckoutCtrl", -> beforeEach -> module("Darkswarm") - order = {} + order = { + submit: -> + } 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 + expect(scope.accordion.user).toEqual true + + it "delegates to the service on submit", -> + event = { + preventDefault: -> + } + spyOn(order, "submit") + scope.purchase(event) + expect(order.submit).toHaveBeenCalled() diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 1e0dc387ed..f6190bdfd0 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -1,6 +1,7 @@ describe 'Order service', -> Order = null orderData = null + $httpBackend = null beforeEach -> orderData = { @@ -20,28 +21,34 @@ describe 'Order service', -> } angular.module('Darkswarm').value('order', orderData) module 'Darkswarm' - inject ($injector)-> + inject ($injector, _$httpBackend_)-> + $httpBackend = _$httpBackend_ Order = $injector.get("Order") it "defaults the shipping method to the first", -> - expect(Order.shipping_method_id).toEqual 7 + expect(Order.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 + expect(Order.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 + Order.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 + Order.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 + Order.order.payment_method_id = 99 expect(Order.paymentMethod()).toEqual {test: "foo"} + it "Posts the Order to the server", -> + $httpBackend.expectPUT("/shop/checkout", {order: Order.preprocess()}).respond 200 + Order.submit() + $httpBackend.flush() + From 36a4e34dffc1dd9e5d66ba743eda26128111a34b Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 9 Apr 2014 15:04:17 +1000 Subject: [PATCH 49/75] Preprocessing order attributes, returning path --- .../darkswarm/services/order.js.coffee | 19 ++++++++++++++----- app/controllers/shop/checkout_controller.rb | 10 ++++++++-- app/views/shop/checkout/edit.html.haml | 3 +++ .../shop/checkout_controller_spec.rb | 11 ++++++++++- .../darkswarm/services/order_spec.js.coffee | 7 +++++++ 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 6310d875bb..913b0ae789 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -8,15 +8,24 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> @order.ship_address_same_as_billing ?= true submit: -> - $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)-> - console.log "success" + $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> console.log data - .error (data, status)-> + # Navigate to order confirmation + .error (errors, status)=> console.log "error" - console.log data + @errors = errors + # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> - @order + munged_order = {} + for name, value of @order # Clone all data from the order JSON object + if name == "bill_address" + munged_order["bill_address_attributes"] = value + else if name == "ship_address" + munged_order["ship_address_attributes"] = value + else + munged_order[name] = value + munged_order shippingMethod: -> @order.shipping_methods[@order.shipping_method_id] diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb index 2199d4be03..0ffe90a91f 100644 --- a/app/controllers/shop/checkout_controller.rb +++ b/app/controllers/shop/checkout_controller.rb @@ -26,10 +26,16 @@ class Shop::CheckoutController < Spree::CheckoutController return end end - if @order.state == "complete" || @order.completed? flash.notice = t(:order_processed_successfully) - respond_with(@order, :location => order_path(@order)) + respond_to do |format| + format.html do + respond_with(@order, :location => order_path(@order)) + end + format.js do + render json: {path: order_path(@order)}, status: 200 + end + end else update_failed end diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 17ded5cc12..6d1ef446c7 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -8,6 +8,9 @@ %accordion.row{"close-others" => "true"} %checkout{"ng-controller" => "CheckoutCtrl"} + + %pre + {{ Order.errors | json }} .large-9.columns - unless spree_current_user = render partial: "shop/checkout/authentication" diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index c6d6851208..31f96587c9 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -81,7 +81,6 @@ describe Shop::CheckoutController do context "via xhr" do before do controller.stub(:current_distributor).and_return(distributor) - controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return(order) end @@ -91,6 +90,16 @@ describe Shop::CheckoutController do response.status.should == 400 response.body.should == assigns[:order].errors.to_json end + + it "returns order confirmation url on success" do + order.stub(:update_attributes).and_return true + order.stub(:state).and_return "complete" + #order.stub(:completed?).and_return true + + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 200 + response.body.should == {path: spree.order_path(order)}.to_json + end end describe "Paypal routing" do diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index f6190bdfd0..5754705c14 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -7,6 +7,8 @@ describe 'Order service', -> orderData = { id: 3102 payment_method_id: null + bill_address: {} + ship_address: {} shipping_methods: 7: require_ship_address: true @@ -52,3 +54,8 @@ describe 'Order service', -> Order.submit() $httpBackend.flush() + it "Munges the order attributes to add _attributes as Rails needs", -> + expect(Order.preprocess().bill_address_attributes).not.toBe(undefined) + expect(Order.preprocess().bill_address).toBe(undefined) + expect(Order.preprocess().ship_address_attributes).not.toBe(undefined) + expect(Order.preprocess().ship_address).toBe(undefined) From 873de1d1181f8a459e7c1f1477f4d4d4ef9053f8 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 9 Apr 2014 15:31:15 +1000 Subject: [PATCH 50/75] Able to submit Checkout form via Ajax --- app/assets/javascripts/darkswarm/services/order.js.coffee | 4 +++- app/views/shop/checkout/_billing.html.haml | 2 +- spec/controllers/shop/checkout_controller_spec.rb | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 913b0ae789..72c69bb589 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -10,7 +10,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> submit: -> $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> console.log data - # Navigate to order confirmation + window.location.pathname = data.path .error (errors, status)=> console.log "error" @errors = errors @@ -23,6 +23,8 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> munged_order["bill_address_attributes"] = value else if name == "ship_address" munged_order["ship_address_attributes"] = value + else if name == "payment_method_id" + munged_order["payments_attributes"] = [{payment_method_id: value}] else munged_order[name] = value munged_order diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index 1bbbc3fd39..f05908ddf4 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -23,7 +23,7 @@ "ng-model" => "order.bill_address.city" .large-6.columns - = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, + = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, "ng-model" => "order.bill_address.state_id" .row .large-6.columns diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index 31f96587c9..d5c5ec60a5 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -94,7 +94,6 @@ describe Shop::CheckoutController do it "returns order confirmation url on success" do order.stub(:update_attributes).and_return true order.stub(:state).and_return "complete" - #order.stub(:completed?).and_return true xhr :post, :update, order: {}, use_route: :spree response.status.should == 200 From 621e58f67a603a38b6c10138b7dd449b6a810f4d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 12:54:18 +1000 Subject: [PATCH 51/75] First stab at a DRY implementation --- .../controllers/checkout_controller.js.coffee | 73 ++++++------------- app/views/shop/checkout/_details.html.haml | 29 +++++--- app/views/shop/checkout/_form.html.haml | 2 +- .../checkout_controller_spec.js.coffee | 22 +++++- .../darkswarm/services/order_spec.js.coffee | 2 +- 5 files changed, 67 insertions(+), 61 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index b3af7a44eb..dae2727320 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -13,64 +13,39 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" - # Validation utilities to keep things DRY - $scope.dirtyInvalid = (name)-> - $scope.dirty(name) and $scope.invalid(name) - $scope.dirty = (name)-> - $scope.checkout[name].$dirty - $scope.invalid = (name)-> - $scope.checkout[name].$invalid - # Validations + $scope.fieldValid = (path)-> + not ($scope.dirty(path) and $scope.invalid(path)) + $scope.field = (path)-> + $scope.checkout[path] + $scope.dirty = (name)-> + $scope.field(name).$dirty + $scope.invalid = (name)-> + $scope.field(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 + $scope.fieldErrors = (path)-> + errors = for error, invalid of $scope.error(path) + if invalid + switch error + when "required" then "must not be blank" + when "number" then "must be number" + when "email" then "must be email address" + (errors.filter (error) -> error?).join ", " $scope.purchase = (event)-> event.preventDefault() $scope.Order.submit() -# READ THIS FIRST -# https://github.com/angular/angular.js/wiki/Understanding-Scopes - Darkswarm.controller "DetailsSubCtrl", ($scope) -> - $scope.detailsValid = -> - $scope.detailsFields().every (field)-> - $scope.checkout[field].$valid + #$scope.detailsValid = -> + #$scope.detailsFields().every (field)-> + #$scope.checkout[field].$valid - $scope.$watch -> - $scope.detailsValid() - , (valid)-> - if valid - $scope.show("billing") + #$scope.$watch -> + #$scope.detailsValid() + #, (valid)-> + #if valid + #$scope.show("billing") - $scope.detailsFields = -> - - {"order[email]" : {email: "must be email", required: "field required"}} - - ["order[email]", - "order[bill_address_attributes][phone]", - "order[bill_address_attributes][firstname]", - "order[bill_address_attributes][lastname]"] - - $scope.emailName = 'order[email]' - $scope.emailValid = -> - $scope.dirtyInvalid($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.dirtyInvalid($scope.phoneName) - $scope.phoneError = -> - "must be a number" - - - diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index ef5fb98d53..a49dab8337 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -10,18 +10,29 @@ {{ order.bill_address.lastname }} .row .large-6.columns - = f.email_field :email, required: "", "ng-model" => "order.email", - "ng-class" => "{error: '!emailValid()'}" - - %small.error.medium.input-text{"ng-show" => "emailValid()"} - {{ emailError() }} + + - path = "order.email" + %label{for: path} Email + %input.medium.input-text{name: path, + "ng-model" => path, + required: true, + type: :email, + "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} + %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" = 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() }} + + - path = "order.bill_address.phone" + %label{for: path} Phone + %input.medium.input-text{name: path, + "ng-model" => path, + required: true, + type: :text, + "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} + %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" = f.fields_for :bill_address, @order.bill_address do |ba| .row diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index faf66d578f..a93b118ff1 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -1,7 +1,7 @@ = f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form", - novalidate: "", + novalidate: true, name: "checkout"} do |f| :javascript 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 c6efca67b1..9c267f4976 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -1,7 +1,7 @@ describe "CheckoutCtrl", -> ctrl = null scope = null - order = null + order = null beforeEach -> module("Darkswarm") @@ -22,3 +22,23 @@ describe "CheckoutCtrl", -> spyOn(order, "submit") scope.purchase(event) expect(order.submit).toHaveBeenCalled() + + it "finds a field by path", -> + scope.checkout = + path: "test" + expect(scope.field('path')).toEqual "test" + + it "tests validity", -> + scope.checkout = + path: + $dirty: true + $invalid: true + expect(scope.fieldValid('path')).toEqual false + + it "returns errors by path", -> + scope.checkout = + path: + $error: + email: true + required: true + expect(scope.fieldErrors('path')).toEqual ["must be email address", "must not be blank"].join ", " diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 5754705c14..3606fcef29 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -50,7 +50,7 @@ describe 'Order service', -> expect(Order.paymentMethod()).toEqual {test: "foo"} it "Posts the Order to the server", -> - $httpBackend.expectPUT("/shop/checkout", {order: Order.preprocess()}).respond 200 + $httpBackend.expectPUT("/shop/checkout", {order: Order.preprocess()}).respond 200, {path: "test"} Order.submit() $httpBackend.flush() From 8b40092310a7acd24546a22c37fd7fb8e2334498 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 14:27:32 +1000 Subject: [PATCH 52/75] Starting to reflow the controllers --- .../javascripts/darkswarm/all.js.coffee | 1 + .../checkout/details_controller.js.coffee | 15 +++++ .../controllers/checkout_controller.js.coffee | 23 ++++--- .../darkswarm/mixins/fieldset_mixin.js.coffee | 28 +++++++++ app/views/shop/checkout/_details.html.haml | 61 ++++++++++--------- 5 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee create mode 100644 app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 6fffd3563a..89e4b1d807 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -14,6 +14,7 @@ #= require ../shared/jquery.timeago #= require foundation #= require ./darkswarm +#= require_tree ./mixins #= require_tree . $ -> diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee new file mode 100644 index 0000000000..5361950bb6 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee @@ -0,0 +1,15 @@ +Darkswarm.controller "DetailsCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "details" + + + #$scope.detailsValid = -> + #$scope.detailsFields().every (field)-> + #$scope.checkout[field].$valid + + #$scope.$watch -> + #$scope.detailsValid() + #, (valid)-> + #if valid + #$scope.show("billing") + diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index dae2727320..ce65313eb7 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -13,18 +13,28 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" + $scope.purchase = (event)-> + event.preventDefault() + $scope.Order.submit() + +FieldsetController = ($scope)-> + $scope.field = (path)-> + $scope[$scope.name][path] $scope.fieldValid = (path)-> not ($scope.dirty(path) and $scope.invalid(path)) - $scope.field = (path)-> - $scope.checkout[path] + $scope.dirty = (name)-> $scope.field(name).$dirty + $scope.invalid = (name)-> $scope.field(name).$invalid + $scope.error = (name)-> - $scope.checkout[name].$error + $scope.field(name).$error + $scope.fieldErrors = (path)-> + # TODO: display server errors errors = for error, invalid of $scope.error(path) if invalid switch error @@ -33,12 +43,11 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> when "email" then "must be email address" (errors.filter (error) -> error?).join ", " - $scope.purchase = (event)-> - event.preventDefault() - $scope.Order.submit() Darkswarm.controller "DetailsSubCtrl", ($scope) -> + angular.extend(this, new FieldsetController($scope)) + $scope.name = "details" #$scope.detailsValid = -> #$scope.detailsFields().every (field)-> #$scope.checkout[field].$valid @@ -48,4 +57,4 @@ Darkswarm.controller "DetailsSubCtrl", ($scope) -> #, (valid)-> #if valid #$scope.show("billing") - + diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee new file mode 100644 index 0000000000..6bc0424744 --- /dev/null +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -0,0 +1,28 @@ +window.FieldsetMixin = ($scope)-> + $scope.field = (path)-> + $scope[$scope.name][path] + + $scope.fieldValid = (path)-> + not ($scope.dirty(path) and $scope.invalid(path)) + + $scope.dirty = (name)-> + $scope.field(name).$dirty + + $scope.invalid = (name)-> + $scope.field(name).$invalid + + $scope.error = (name)-> + $scope.field(name).$error + + $scope.fieldErrors = (path)-> + # TODO: display server errors + errors = for error, invalid of $scope.error(path) + if invalid + switch error + when "required" then "must not be blank" + when "number" then "must be number" + when "email" then "must be email address" + (errors.filter (error) -> error?).join ", " + + + diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index a49dab8337..94639d5607 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -1,43 +1,44 @@ %fieldset#details %accordion-group{"is-open" => "accordion.details"} - %div{"ng-controller" => "DetailsSubCtrl"} - %accordion-heading + %div{"ng-controller" => "DetailsCtrl"} + %ng-form{name: "details"} + %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 - - path = "order.email" - %label{for: path} Email - %input.medium.input-text{name: path, - "ng-model" => path, - required: true, - type: :email, - "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} - %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} - = "{{ fieldErrors('#{path}') }}" - - = f.fields_for :bill_address, @order.bill_address do |ba| - .large-6.columns - - - path = "order.bill_address.phone" - %label{for: path} Phone + - path = "order.email" + %label{for: path} Email %input.medium.input-text{name: path, "ng-model" => path, required: true, - type: :text, + type: :email, "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} = "{{ fieldErrors('#{path}') }}" - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-6.columns - = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" + = f.fields_for :bill_address, @order.bill_address do |ba| + .large-6.columns - .large-6.columns - = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" + - path = "order.bill_address.phone" + %label{for: path} Phone + %input.medium.input-text{name: path, + "ng-model" => path, + required: true, + type: :text, + "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} + %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" + + = 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" From 00f5d09d651842b697d19c9af9dcdcbeebcf1895 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 14:48:22 +1000 Subject: [PATCH 53/75] Moving our inputs to magical helpers --- .../checkout/details_controller.js.coffee | 4 -- .../controllers/checkout_controller.js.coffee | 41 ------------------- app/helpers/checkout_helper.rb | 8 ++++ app/views/shared/_validated_input.html.haml | 8 ++++ app/views/shop/checkout/_details.html.haml | 23 +---------- spec/helpers/checkout_helper_spec.rb | 13 ++++++ 6 files changed, 31 insertions(+), 66 deletions(-) create mode 100644 app/views/shared/_validated_input.html.haml create mode 100644 spec/helpers/checkout_helper_spec.rb diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee index 5361950bb6..a218e123b1 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee @@ -2,10 +2,6 @@ Darkswarm.controller "DetailsCtrl", ($scope) -> angular.extend(this, new FieldsetMixin($scope)) $scope.name = "details" - - #$scope.detailsValid = -> - #$scope.detailsFields().every (field)-> - #$scope.checkout[field].$valid #$scope.$watch -> #$scope.detailsValid() diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index ce65313eb7..bbde0d123a 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -17,44 +17,3 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> event.preventDefault() $scope.Order.submit() -FieldsetController = ($scope)-> - $scope.field = (path)-> - $scope[$scope.name][path] - - $scope.fieldValid = (path)-> - not ($scope.dirty(path) and $scope.invalid(path)) - - $scope.dirty = (name)-> - $scope.field(name).$dirty - - $scope.invalid = (name)-> - $scope.field(name).$invalid - - $scope.error = (name)-> - $scope.field(name).$error - - $scope.fieldErrors = (path)-> - # TODO: display server errors - errors = for error, invalid of $scope.error(path) - if invalid - switch error - when "required" then "must not be blank" - when "number" then "must be number" - when "email" then "must be email address" - (errors.filter (error) -> error?).join ", " - - - -Darkswarm.controller "DetailsSubCtrl", ($scope) -> - angular.extend(this, new FieldsetController($scope)) - $scope.name = "details" - #$scope.detailsValid = -> - #$scope.detailsFields().every (field)-> - #$scope.checkout[field].$valid - - #$scope.$watch -> - #$scope.detailsValid() - #, (valid)-> - #if valid - #$scope.show("billing") - diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index b21855bf7a..6cf2aca961 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -10,4 +10,12 @@ module CheckoutHelper adjustments end + + def validated_input(name, path, args = {}) + defaults = { + required: true, + type: :text + }.merge args + render partial: "shared/validated_input", locals: {name: name, path: path}.merge(defaults) + end end diff --git a/app/views/shared/_validated_input.html.haml b/app/views/shared/_validated_input.html.haml new file mode 100644 index 0000000000..eb4c524a65 --- /dev/null +++ b/app/views/shared/_validated_input.html.haml @@ -0,0 +1,8 @@ +%label{for: path}= name +%input.medium.input-text{name: path, + "ng-model" => path, + required: required, + type: type, + "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} +%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 94639d5607..74f0ad2c26 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -11,29 +11,10 @@ {{ order.bill_address.lastname }} .row .large-6.columns - - - path = "order.email" - %label{for: path} Email - %input.medium.input-text{name: path, - "ng-model" => path, - required: true, - type: :email, - "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} - %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} - = "{{ fieldErrors('#{path}') }}" - + = validated_input('Email', 'order.email', type: :email) = f.fields_for :bill_address, @order.bill_address do |ba| .large-6.columns - - - path = "order.bill_address.phone" - %label{for: path} Phone - %input.medium.input-text{name: path, - "ng-model" => path, - required: true, - type: :text, - "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} - %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} - = "{{ fieldErrors('#{path}') }}" + = validated_input 'Phone', 'order.bill_address.phone' = f.fields_for :bill_address, @order.bill_address do |ba| .row diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb new file mode 100644 index 0000000000..a01d2e48d4 --- /dev/null +++ b/spec/helpers/checkout_helper_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + + +describe CheckoutHelper do + it "generates html for validated inputs" do + helper.should_receive(:render).with( + partial: "shared/validated_input", + locals: {name: "test", path: "foo", required: true, type: :email} + ) + + helper.validated_input("test", "foo", type: :email) + end +end From 5f62cb7ddba6632a32d2e1a8c2ee776e02ed0403 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 16:32:08 +1000 Subject: [PATCH 54/75] Working with ngForm and subcontrollers. Also fixing bulk order specs --- .../admin/bulk_order_management.js.coffee | 2 +- .../checkout/billing_controller.js.coffee | 3 + .../checkout/shipping_controller.js.coffee | 3 + .../controllers/checkout_controller.js.coffee | 4 +- .../darkswarm/mixins/fieldset_mixin.js.coffee | 11 +- .../darkswarm/services/order.js.coffee | 16 +- .../stylesheets/darkswarm/checkout.css.sass | 3 + app/views/shop/checkout/_billing.html.haml | 60 ++++---- app/views/shop/checkout/_details.html.haml | 39 ++--- app/views/shop/checkout/_shipping.html.haml | 143 +++++++++--------- app/views/shop/checkout/edit.html.haml | 4 +- .../unit/bulk_order_management_spec.js.coffee | 23 ++- .../details_controller_spec.js.coffee | 31 ++++ .../checkout_controller_spec.js.coffee | 21 +-- .../darkswarm/services/order_spec.js.coffee | 14 +- 15 files changed, 205 insertions(+), 172 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee create mode 100644 app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee create mode 100644 spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 22cc1660cf..2d97b68ed7 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -338,4 +338,4 @@ formatDate = (date) -> twoDigitNumber = (number) -> twoDigits = "" + number twoDigits = ("0" + number) if number < 10 - twoDigits \ No newline at end of file + twoDigits diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee new file mode 100644 index 0000000000..2c07d96da5 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "BillingCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "billing" diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee new file mode 100644 index 0000000000..82b435852b --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "ShippingCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "shipping" diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index bbde0d123a..7ad47f5398 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,5 +1,4 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> - window.tmp = $scope $scope.Order = Order $scope.order = Order.order $scope.accordion = {} @@ -12,8 +11,9 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "accordion.billing" storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" + storage.bind $scope, "order.ship_address_same_as_billing", { defaultValue: true} + storage.bind $scope, "order.shipping_method_id" $scope.purchase = (event)-> event.preventDefault() $scope.Order.submit() - diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index 6bc0424744..0bd1b78ef3 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -1,6 +1,12 @@ window.FieldsetMixin = ($scope)-> + $scope.valid = -> + $scope.form().$valid + + $scope.form = -> + $scope[$scope.name] + $scope.field = (path)-> - $scope[$scope.name][path] + $scope.form()[path] $scope.fieldValid = (path)-> not ($scope.dirty(path) and $scope.invalid(path)) @@ -23,6 +29,3 @@ window.FieldsetMixin = ($scope)-> when "number" then "must be number" when "email" then "must be email address" (errors.filter (error) -> error?).join ", " - - - diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 72c69bb589..e5ae4d4bb1 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -3,17 +3,21 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> errors: {} constructor: -> @order = order - # Here we default to the first shipping method if none is selected + # Default to first shipping method if none selected @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) - @order.ship_address_same_as_billing ?= true + + navigate: (path)-> + console.log path + window.location.pathname = path submit: -> $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> - console.log data - window.location.pathname = data.path + @navigate(data.path) .error (errors, status)=> console.log "error" @errors = errors + + # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> @@ -27,6 +31,9 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> munged_order["payments_attributes"] = [{payment_method_id: value}] else munged_order[name] = value + # TODO: this + if munged_order.ship_address_same_as_billing + munged_order.ship_address_attributes = munged_order.bill_address_attributes munged_order shippingMethod: -> @@ -43,4 +50,3 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> cartTotal: -> @shippingPrice() + @order.display_total - diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index 676ab08e43..1d6819d57a 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -4,3 +4,6 @@ checkout orderdetails .button, table width: 100% + + dd.valid + background: green diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index f05908ddf4..b32c0d2498 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -1,34 +1,32 @@ %fieldset#billing - %accordion-group{"is-open" => "accordion.billing"} - %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 + %ng-form{"ng-controller" => "BillingCtrl", name: "billing"} + %accordion-group{"is-open" => "accordion.billing", + "ng-class" => "{valid: billing.$valid}"} + %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 + = validated_input "Address", "order.bill_address.address1" + .row + .large-12.columns + = validated_input "Address (contd.)", "order.bill_address.address2", required: false + .row + .large-6.columns + = validated_input "City", "order.bill_address.city" - = 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]}, {include_blank: false}, + "ng-model" => "order.bill_address.state_id" + .row + .large-6.columns + = validated_input "Postcode", "order.bill_address.zipcode" - .large-6.columns - = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, - "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" + .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 index 74f0ad2c26..0faa40e858 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -1,25 +1,26 @@ %fieldset#details - %accordion-group{"is-open" => "accordion.details"} - %div{"ng-controller" => "DetailsCtrl"} - %ng-form{name: "details"} - %accordion-heading - .row - .large-6.columns - Customer Details - .large-6.columns.text-right - {{ order.bill_address.firstname }} - {{ order.bill_address.lastname }} + %ng-form{"ng-controller" => "DetailsCtrl", name: "details"} + %accordion-group{"is-open" => "accordion.details", + "ng-class" => "{valid: details.$valid}"} + %accordion-heading .row .large-6.columns - = validated_input('Email', 'order.email', type: :email) - = f.fields_for :bill_address, @order.bill_address do |ba| - .large-6.columns - = validated_input 'Phone', 'order.bill_address.phone' + Customer Details + .large-6.columns.text-right + {{ order.bill_address.firstname }} + {{ order.bill_address.lastname }} + .row + .large-6.columns + = validated_input('Email', 'order.email', type: :email) = 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 + = validated_input 'Phone', 'order.bill_address.phone' - .large-6.columns - = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .large-6.columns + = validated_input "First Name", "order.bill_address.firstname" + + .large-6.columns + = validated_input "Last Name", "order.bill_address.lastname" diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 62aed1d235..06bad5836b 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -1,76 +1,75 @@ %fieldset#shipping - %accordion-group{"is-open" => "shipping"} - %accordion-heading - .row - .large-6.columns - Shipping - .large-6.columns.text-right - {{ Order.shippingMethod().name }} - - for ship_method, i in current_distributor.shipping_methods.uniq - .row - .large-12.columns + %ng-form{"ng-controller" => "ShippingCtrl", name: "shipping"} + %accordion-group{"is-open" => "accordion.shipping", + "ng-class" => "{valid: shipping.$valid}"} + %accordion-heading + .row + .large-6.columns + Shipping + .large-6.columns.text-right + {{ Order.shippingMethod().name }} + - for ship_method, i in current_distributor.shipping_methods.uniq + .row + .large-12.columns + %label + = radio_button_tag "order[shipping_method_id]", ship_method.id, false, + "ng-model" => "order.shipping_method_id" + = ship_method.name + + #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.requireShipAddress()"} %label - = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-model" => "order.shipping_method_id" - = ship_method.name + = 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? - #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) + %div.visible{"ng-show" => "!order.ship_address_same_as_billing"} + .row + .large-12.columns + = validated_input "Address", "order.ship_address.address1" + .row + .large-12.columns + = validated_input "Address (contd.)", "order.ship_address.address2" + .row + .large-6.columns + = validated_input "City", "order.ship_address.city" + .large-6.columns + = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} + .row + .large-6.columns + = validated_input "Postcode", "order.ship_address.zipcode" + .large-6.columns.right + = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, + {include_blank: false} + .row + .large-6.columns + = validated_input "First Name", "order.ship_address.firstname" + .large-6.columns + = validated_input "Last Name", "order.ship_address.last" + .row + .large-6.columns + = validated_input "Phone", "order.ship_address.phone" - = f.fields_for :ship_address, @order.ship_address do |sa| - - #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, - "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" + -##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 6d1ef446c7..5adefc5fdb 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -8,9 +8,7 @@ %accordion.row{"close-others" => "true"} %checkout{"ng-controller" => "CheckoutCtrl"} - - %pre - {{ Order.errors | json }} + {{ order }} .large-9.columns - unless spree_current_user = render partial: "shop/checkout/authentication" diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index f75e6ed6fe..b770e9b7b1 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -23,20 +23,19 @@ describe "AdminOrderMgmtCtrl", -> httpBackend.expectGET("/api/order_cycles/managed").respond returnedOrderCycles spyOn(scope, "initialiseVariables").andCallThrough() spyOn(scope, "fetchOrders").andReturn "nothing" - spyOn(returnedSuppliers, "unshift") - spyOn(returnedDistributors, "unshift") - spyOn(returnedOrderCycles, "unshift") + #spyOn(returnedSuppliers, "unshift") + #spyOn(returnedDistributors, "unshift") + #spyOn(returnedOrderCycles, "unshift") scope.initialise "api_key" httpBackend.flush() - #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 - expect(scope.fetchOrders.calls.length).toEqual 1 - expect(returnedSuppliers.unshift.calls.length).toEqual 1 - expect(returnedDistributors.unshift.calls.length).toEqual 1 - expect(returnedOrderCycles.unshift.calls.length).toEqual 1 - expect(scope.spree_api_key_ok).toEqual true + + expect(scope.suppliers).toEqual [{ id : '', name : 'All' }, 'list of suppliers'] + expect(scope.distributors).toEqual [ { id : '', name : 'All' }, 'list of distributors' ] + expect(scope.orderCycles).toEqual [ { id : '', name : 'All' }, 'oc1', 'oc2', 'oc3' ] + + expect(scope.initialiseVariables.calls.length).toBe 1 + expect(scope.fetchOrders.calls.length).toBe 1 + expect(scope.spree_api_key_ok).toBe true describe "fetching orders", -> beforeEach -> diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee new file mode 100644 index 0000000000..a536988720 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee @@ -0,0 +1,31 @@ +describe "DetailsCtrl", -> + ctrl = null + scope = null + order = null + + beforeEach -> + module("Darkswarm") + inject ($controller, $rootScope) -> + scope = $rootScope.$new() + ctrl = $controller 'DetailsCtrl', {$scope: scope} + + + it "finds a field by path", -> + scope.details = + path: "test" + expect(scope.field('path')).toEqual "test" + + it "tests validity", -> + scope.details = + path: + $dirty: true + $invalid: true + expect(scope.fieldValid('path')).toEqual false + + it "returns errors by path", -> + scope.details = + path: + $error: + email: true + required: true + expect(scope.fieldErrors('path')).toEqual ["must be email address", "must not be blank"].join ", " 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 9c267f4976..b4df9d4b9f 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -7,6 +7,7 @@ describe "CheckoutCtrl", -> module("Darkswarm") order = { submit: -> + navigate: -> } inject ($controller, $rootScope) -> scope = $rootScope.$new() @@ -22,23 +23,3 @@ describe "CheckoutCtrl", -> spyOn(order, "submit") scope.purchase(event) expect(order.submit).toHaveBeenCalled() - - it "finds a field by path", -> - scope.checkout = - path: "test" - expect(scope.field('path')).toEqual "test" - - it "tests validity", -> - scope.checkout = - path: - $dirty: true - $invalid: true - expect(scope.fieldValid('path')).toEqual false - - it "returns errors by path", -> - scope.checkout = - path: - $error: - email: true - required: true - expect(scope.fieldErrors('path')).toEqual ["must be email address", "must not be blank"].join ", " diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 3606fcef29..9559af0fd2 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -7,8 +7,8 @@ describe 'Order service', -> orderData = { id: 3102 payment_method_id: null - bill_address: {} - ship_address: {} + bill_address: {test: "foo"} + ship_address: {test: "bar"} shipping_methods: 7: require_ship_address: true @@ -26,12 +26,14 @@ describe 'Order service', -> inject ($injector, _$httpBackend_)-> $httpBackend = _$httpBackend_ Order = $injector.get("Order") + spyOn(Order, "navigate") # Stubbing out writes to window.location it "defaults the shipping method to the first", -> expect(Order.order.shipping_method_id).toEqual 7 expect(Order.shippingMethod()).toEqual { require_ship_address : true, price : 0 } - it "defaults to 'same as billing' for address", -> + # This is now handled via localStorage defaults + xit "defaults to 'same as billing' for address", -> expect(Order.order.ship_address_same_as_billing).toEqual true it 'Tracks whether a ship address is required', -> @@ -59,3 +61,9 @@ describe 'Order service', -> expect(Order.preprocess().bill_address).toBe(undefined) expect(Order.preprocess().ship_address_attributes).not.toBe(undefined) expect(Order.preprocess().ship_address).toBe(undefined) + + it "Munges the order attributes to clone ship address from bill address", -> + Order.order.ship_address_same_as_billing = false + expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address) + Order.order.ship_address_same_as_billing = true + expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address) From acd8d2d8a2604be1a28825ad2f90ec5a37efde4d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 16:35:03 +1000 Subject: [PATCH 55/75] Adding payment controller for consistency --- .../checkout/payment_controller.js.coffee | 3 ++ app/views/shop/checkout/_payment.html.haml | 35 ++++++++++--------- app/views/shop/checkout/edit.html.haml | 2 -- 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee new file mode 100644 index 0000000000..ee5ff84b4a --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "PaymentCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "payment" diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml index e0efa6a165..78b0e9792f 100644 --- a/app/views/shop/checkout/_payment.html.haml +++ b/app/views/shop/checkout/_payment.html.haml @@ -1,19 +1,20 @@ %fieldset#payment - %accordion-group - %accordion-heading - .row - .large-6.columns - Payment Details - .large-6.columns.text-right - {{ Order.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 } + %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} + %accordion-group + %accordion-heading + .row + .large-6.columns + Payment Details + .large-6.columns.text-right + {{ Order.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/edit.html.haml b/app/views/shop/checkout/edit.html.haml index 5adefc5fdb..c3db3fddf6 100644 --- a/app/views/shop/checkout/edit.html.haml +++ b/app/views/shop/checkout/edit.html.haml @@ -8,13 +8,11 @@ %accordion.row{"close-others" => "true"} %checkout{"ng-controller" => "CheckoutCtrl"} - {{ order }} .large-9.columns - unless spree_current_user = render partial: "shop/checkout/authentication" .row = render partial: "shop/checkout/form" - .large-3.columns = render partial: "shop/checkout/summary" From 7fc3d4b7cc38182338e62c0752d9abf62d6bb49d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 17:02:43 +1000 Subject: [PATCH 56/75] Moving to a switch --- .../darkswarm/services/order.js.coffee | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index e5ae4d4bb1..61604e3dbb 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -7,31 +7,28 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) navigate: (path)-> - console.log path window.location.pathname = path submit: -> $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> @navigate(data.path) .error (errors, status)=> - console.log "error" @errors = errors - - # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> munged_order = {} for name, value of @order # Clone all data from the order JSON object - if name == "bill_address" - munged_order["bill_address_attributes"] = value - else if name == "ship_address" - munged_order["ship_address_attributes"] = value - else if name == "payment_method_id" - munged_order["payments_attributes"] = [{payment_method_id: value}] - else - munged_order[name] = value - # TODO: this + switch name + when "bill_address" + munged_order["bill_address_attributes"] = value + when "ship_address" + munged_order["ship_address_attributes"] = value + when "payment_method_id" + munged_order["payments_attributes"] = [{payment_method_id: value}] + else + munged_order[name] = value + if munged_order.ship_address_same_as_billing munged_order.ship_address_attributes = munged_order.bill_address_attributes munged_order From 831e53dc6e54a95a760ec3fa8bdf89eacf0950b9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 17:21:34 +1000 Subject: [PATCH 57/75] Displaying server errors as well --- .../darkswarm/mixins/fieldset_mixin.js.coffee | 9 ++++--- .../javascripts/darkswarm/overrides.js.coffee | 24 ++++--------------- app/views/shop/checkout/_shipping.html.haml | 2 +- app/views/shop/checkout/_summary.html.haml | 2 ++ 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index 0bd1b78ef3..96273ffaed 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -21,11 +21,14 @@ window.FieldsetMixin = ($scope)-> $scope.field(name).$error $scope.fieldErrors = (path)-> - # TODO: display server errors errors = for error, invalid of $scope.error(path) if invalid switch error - when "required" then "must not be blank" + when "required" then "can't be blank" when "number" then "must be number" when "email" then "must be email address" - (errors.filter (error) -> error?).join ", " + + server_errors = $scope.Order.errors[path.replace('order.', '')] + errors.push server_errors if server_errors? + (errors.filter (error) -> error?).unique().join ", " + diff --git a/app/assets/javascripts/darkswarm/overrides.js.coffee b/app/assets/javascripts/darkswarm/overrides.js.coffee index 4d678c77a1..b9dd47500b 100644 --- a/app/assets/javascripts/darkswarm/overrides.js.coffee +++ b/app/assets/javascripts/darkswarm/overrides.js.coffee @@ -1,20 +1,4 @@ -#Foundation.libs.section.toggle_active = (e)-> - #$this = $(this) - #self = Foundation.libs.section - #region = $this.parent() - #content = $this.siblings(self.settings.content_selector) - #section = region.parent() - #settings = $.extend({}, self.settings, self.data_options(section)) - #prev_active_region = section.children(self.settings.region_selector).filter("." + self.settings.active_class) - - ##for anchors inside [data-section-title] - #e.preventDefault() if not settings.deep_linking and content.length > 0 - #e.stopPropagation() #do not catch same click again on parent - #unless region.hasClass(self.settings.active_class) - #prev_active_region.removeClass self.settings.active_class - #region.addClass self.settings.active_class - ##force resize for better performance (do not wait timer) - #self.resize region.find(self.settings.section_selector).not("[" + self.settings.resized_data_attr + "]"), true - #else if not settings.one_up# and (self.small(section) or self.is_vertical_nav(section) or self.is_horizontal_nav(section) or self.is_accordion(section)) - #region.removeClass self.settings.active_class - #settings.callback section +Array::unique = -> + output = {} + output[@[key]] = @[key] for key in [0...@length] + value for key, value of output diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 06bad5836b..d5692b1bea 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -51,7 +51,7 @@ .large-6.columns = validated_input "First Name", "order.ship_address.firstname" .large-6.columns - = validated_input "Last Name", "order.ship_address.last" + = validated_input "Last Name", "order.ship_address.lastname" .row .large-6.columns = validated_input "Phone", "order.ship_address.phone" diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 730263a249..97ec4984cd 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -1,5 +1,7 @@ %orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| + + {{ Order.errors }} %fieldset %legend Your Order %table From da241a4eee4ef98df3f386891e13118f78772802 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 10 Apr 2014 17:26:00 +1000 Subject: [PATCH 58/75] Removing some debugging --- app/views/shop/checkout/_summary.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 97ec4984cd..730263a249 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -1,7 +1,5 @@ %orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| - - {{ Order.errors }} %fieldset %legend Your Order %table From c8012e704560d5e24e539d9e629595fd8b1c29e8 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 11 Apr 2014 16:43:21 +1000 Subject: [PATCH 59/75] Improving panel workflow, moving back to simple format for Order service --- .../javascripts/darkswarm/all.js.coffee | 1 + .../checkout/billing_controller.js.coffee | 3 +++ .../checkout/details_controller.js.coffee | 1 + .../controllers/checkout_controller.js.coffee | 9 ++++---- .../darkswarm/mixins/fieldset_mixin.js.coffee | 10 ++++++--- .../darkswarm/services/order.js.coffee | 16 ++++++++------ app/views/shared/_validated_input.html.haml | 2 +- app/views/shop/checkout/_billing.html.haml | 4 ++++ app/views/shop/checkout/_details.html.haml | 4 ++++ app/views/shop/checkout/_shipping.html.haml | 22 ++----------------- spec/javascripts/application_spec.js | 1 + .../details_controller_spec.js.coffee | 6 ++++- .../darkswarm/services/order_spec.js.coffee | 14 ++++++------ 13 files changed, 50 insertions(+), 43 deletions(-) diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 89e4b1d807..e94c975952 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -14,6 +14,7 @@ #= require ../shared/jquery.timeago #= require foundation #= require ./darkswarm +#= require ./overrides #= require_tree ./mixins #= require_tree . diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee index 2c07d96da5..33b9ed9184 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee @@ -1,3 +1,6 @@ Darkswarm.controller "BillingCtrl", ($scope) -> angular.extend(this, new FieldsetMixin($scope)) $scope.name = "billing" + $scope.nextPanel = "shipping" + + diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee index a218e123b1..dbf273e9e8 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee @@ -1,6 +1,7 @@ Darkswarm.controller "DetailsCtrl", ($scope) -> angular.extend(this, new FieldsetMixin($scope)) $scope.name = "details" + $scope.nextPanel = "billing" #$scope.$watch -> diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 7ad47f5398..3b91b5c44f 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,6 +1,6 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> - $scope.Order = Order - $scope.order = Order.order + $scope.order = $scope.Order = Order + #$scope.order = Order.order $scope.accordion = {} $scope.show = (name)-> @@ -11,8 +11,9 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "accordion.billing" storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" - storage.bind $scope, "order.ship_address_same_as_billing", { defaultValue: true} - storage.bind $scope, "order.shipping_method_id" + + storage.bind $scope, "Order.formstate.ship_address_same_as_billing", { defaultValue: true} + storage.bind $scope, "order", {storeName: "order_#{$scope.order.id}"} $scope.purchase = (event)-> event.preventDefault() diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index 96273ffaed..ff01db5483 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -1,4 +1,8 @@ window.FieldsetMixin = ($scope)-> + $scope.next = (event)-> + event.preventDefault() + $scope.show $scope.nextPanel + $scope.valid = -> $scope.form().$valid @@ -28,7 +32,7 @@ window.FieldsetMixin = ($scope)-> when "number" then "must be number" when "email" then "must be email address" - server_errors = $scope.Order.errors[path.replace('order.', '')] - errors.push server_errors if server_errors? - (errors.filter (error) -> error?).unique().join ", " + #server_errors = $scope.Order.errors[path.replace('order.', '')] + #errors.push server_errors if server_errors? + (errors.filter (error) -> error?).join ", " diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 61604e3dbb..46274eb269 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -1,10 +1,12 @@ -Darkswarm.factory 'Order', ($resource, Product, order, $http)-> +Darkswarm.factory 'Order', ($resource, Product, order, $http, storage)-> new class Order errors: {} + form_state: {} + constructor: -> - @order = order + @[key] = val for key, val of order # zip the order data into our service # Default to first shipping method if none selected - @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) + @shipping_method_id ||= parseInt(Object.keys(@shipping_methods)[0]) navigate: (path)-> window.location.pathname = path @@ -18,7 +20,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> munged_order = {} - for name, value of @order # Clone all data from the order JSON object + for name, value of @ # Clone all data from the order JSON object switch name when "bill_address" munged_order["bill_address_attributes"] = value @@ -34,7 +36,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> munged_order shippingMethod: -> - @order.shipping_methods[@order.shipping_method_id] + @shipping_methods[@shipping_method_id] requireShipAddress: -> @shippingMethod()?.require_ship_address @@ -43,7 +45,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http)-> @shippingMethod()?.price paymentMethod: -> - @order.payment_methods[@order.payment_method_id] + @payment_methods[@payment_method_id] cartTotal: -> - @shippingPrice() + @order.display_total + @shippingPrice() + @display_total diff --git a/app/views/shared/_validated_input.html.haml b/app/views/shared/_validated_input.html.haml index eb4c524a65..251c60b975 100644 --- a/app/views/shared/_validated_input.html.haml +++ b/app/views/shared/_validated_input.html.haml @@ -3,6 +3,6 @@ "ng-model" => path, required: required, type: type, - "ng-class" => "{error: '!fieldValid(\"#{path}\")'}"} + "ng-class" => "{error: !fieldValid('#{path}')}"} %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} = "{{ fieldErrors('#{path}') }}" diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index b32c0d2498..fe7232dfbd 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -30,3 +30,7 @@ .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" + + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 0faa40e858..3f3a815452 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -24,3 +24,7 @@ .large-6.columns = validated_input "Last Name", "order.bill_address.lastname" + + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index d5692b1bea..4bafd6ddf5 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -22,14 +22,14 @@ = @order.order_cycle.pickup_instructions_for(@order.distributor) = f.fields_for :ship_address, @order.ship_address do |sa| - #ship_address{"ng-show" => "Order.requireShipAddress()"} + #ship_address{"ng-if" => "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, "ng-model" => "order.ship_address_same_as_billing" Shipping address same as billing address? - %div.visible{"ng-show" => "!order.ship_address_same_as_billing"} + %div.visible{"ng-if" => "!order.ship_address_same_as_billing"} .row .large-12.columns = validated_input "Address", "order.ship_address.address1" @@ -55,21 +55,3 @@ .row .large-6.columns = validated_input "Phone", "order.ship_address.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/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index ec5fef8832..cd070d79e9 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -3,3 +3,4 @@ //= require angular-animate //= require angular-mocks //= require angular-cookies +// diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee index a536988720..b2f4005199 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee @@ -23,9 +23,13 @@ describe "DetailsCtrl", -> expect(scope.fieldValid('path')).toEqual false it "returns errors by path", -> + scope.Order = + errors: -> scope.details = path: $error: email: true required: true - expect(scope.fieldErrors('path')).toEqual ["must be email address", "must not be blank"].join ", " + expect(scope.fieldErrors('path')).toEqual ["must be email address", "can't be blank"].join ", " + + diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 9559af0fd2..181d1acd31 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -29,26 +29,26 @@ describe 'Order service', -> spyOn(Order, "navigate") # Stubbing out writes to window.location it "defaults the shipping method to the first", -> - expect(Order.order.shipping_method_id).toEqual 7 + expect(Order.shipping_method_id).toEqual 7 expect(Order.shippingMethod()).toEqual { require_ship_address : true, price : 0 } # This is now handled via localStorage defaults xit "defaults to 'same as billing' for address", -> - expect(Order.order.ship_address_same_as_billing).toEqual true + expect(Order.ship_address_same_as_billing).toEqual true it 'Tracks whether a ship address is required', -> expect(Order.requireShipAddress()).toEqual true - Order.order.shipping_method_id = 25 + Order.shipping_method_id = 25 expect(Order.requireShipAddress()).toEqual false it 'Gets the current shipping price', -> expect(Order.shippingPrice()).toEqual 0.0 - Order.order.shipping_method_id = 25 + Order.shipping_method_id = 25 expect(Order.shippingPrice()).toEqual 13 it 'Gets the current payment method', -> expect(Order.paymentMethod()).toEqual null - Order.order.payment_method_id = 99 + Order.payment_method_id = 99 expect(Order.paymentMethod()).toEqual {test: "foo"} it "Posts the Order to the server", -> @@ -63,7 +63,7 @@ describe 'Order service', -> expect(Order.preprocess().ship_address).toBe(undefined) it "Munges the order attributes to clone ship address from bill address", -> - Order.order.ship_address_same_as_billing = false + Order.ship_address_same_as_billing = false expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address) - Order.order.ship_address_same_as_billing = true + Order.ship_address_same_as_billing = true expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address) From 2e0485c06434deb86625bf9b907556191650a719 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 11 Apr 2014 17:24:32 +1000 Subject: [PATCH 60/75] Getting the entire order into localStorage: one bug with radio boxes --- .../controllers/checkout_controller.js.coffee | 8 +++++--- .../services/checkout_form_state.js.coffee | 2 ++ .../darkswarm/services/order.js.coffee | 18 +++++++++--------- app/models/spree/order_decorator.rb | 9 +-------- app/views/shop/checkout/_order.rabl | 2 +- app/views/shop/checkout/_shipping.html.haml | 13 +++++++------ .../checkout_controller_spec.js.coffee | 12 +++++++----- .../darkswarm/services/order_spec.js.coffee | 16 +++++++++------- 8 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/services/checkout_form_state.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 3b91b5c44f..d7ce700b9c 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,5 +1,7 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> - $scope.order = $scope.Order = Order +Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage, CheckoutFormState) -> + $scope.Order = Order + $scope.order = Order.order + $scope.CheckoutFormState = CheckoutFormState #$scope.order = Order.order $scope.accordion = {} @@ -12,7 +14,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage) -> storage.bind $scope, "accordion.shipping" storage.bind $scope, "accordion.payment" - storage.bind $scope, "Order.formstate.ship_address_same_as_billing", { defaultValue: true} + storage.bind $scope, "CheckoutFormState.ship_address_same_as_billing", { defaultValue: true} storage.bind $scope, "order", {storeName: "order_#{$scope.order.id}"} $scope.purchase = (event)-> diff --git a/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee new file mode 100644 index 0000000000..b2f16aea7b --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee @@ -0,0 +1,2 @@ +Darkswarm.factory 'CheckoutFormState', ()-> + new class CheckoutFormState diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 46274eb269..2da4983b93 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -1,12 +1,11 @@ -Darkswarm.factory 'Order', ($resource, Product, order, $http, storage)-> +Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState)-> new class Order errors: {} - form_state: {} constructor: -> - @[key] = val for key, val of order # zip the order data into our service + @order = order # Default to first shipping method if none selected - @shipping_method_id ||= parseInt(Object.keys(@shipping_methods)[0]) + #@order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) navigate: (path)-> window.location.pathname = path @@ -20,7 +19,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http, storage)-> # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> munged_order = {} - for name, value of @ # Clone all data from the order JSON object + for name, value of @order # Clone all data from the order JSON object switch name when "bill_address" munged_order["bill_address_attributes"] = value @@ -28,15 +27,16 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http, storage)-> munged_order["ship_address_attributes"] = value when "payment_method_id" munged_order["payments_attributes"] = [{payment_method_id: value}] + when "form_state" # don't keep this shit else munged_order[name] = value - if munged_order.ship_address_same_as_billing + if CheckoutFormState.ship_address_same_as_billing munged_order.ship_address_attributes = munged_order.bill_address_attributes munged_order shippingMethod: -> - @shipping_methods[@shipping_method_id] + @order.shipping_methods[@order.shipping_method_id] requireShipAddress: -> @shippingMethod()?.require_ship_address @@ -45,7 +45,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http, storage)-> @shippingMethod()?.price paymentMethod: -> - @payment_methods[@payment_method_id] + @order.payment_methods[@order.payment_method_id] cartTotal: -> - @shippingPrice() + @display_total + @shippingPrice() + @order.display_total diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index ccc827ec57..5c94ac8c57 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -10,8 +10,7 @@ Spree::Order.class_eval do belongs_to :cart validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? } - attr_accessible :order_cycle_id, :distributor_id, :ship_address_same_as_billing - attr_accessor :ship_address_same_as_billing + attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor @@ -65,12 +64,6 @@ Spree::Order.class_eval do where("state != ?", state) } - # Accessors - # - def ship_address_same_as_billing=(string_value) - @ship_address_same_as_billing = (string_value == "true") - end - # -- Methods def products_available_from_new_distribution diff --git a/app/views/shop/checkout/_order.rabl b/app/views/shop/checkout/_order.rabl index 48aef7b11e..27ad32c6e3 100644 --- a/app/views/shop/checkout/_order.rabl +++ b/app/views/shop/checkout/_order.rabl @@ -1,5 +1,5 @@ object current_order -attributes :id, :email, :shipping_method_id, :ship_address_same_as_billing +attributes :id, :email, :shipping_method_id node :display_total do current_order.display_total.money.to_f diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 4bafd6ddf5..7893f23ea3 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -1,4 +1,5 @@ %fieldset#shipping + %pre %ng-form{"ng-controller" => "ShippingCtrl", name: "shipping"} %accordion-group{"is-open" => "accordion.shipping", "ng-class" => "{valid: shipping.$valid}"} @@ -12,8 +13,10 @@ .row .large-12.columns %label - = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-model" => "order.shipping_method_id" + -#= radio_button_tag "order[shipping_method_id]", ship_method.id, false, + -#"ng-model" => "order.shipping_method_id" + %input{type: :radio, value: ship_method.id, + "ng-model" => "order.shipping_method_id"} = ship_method.name #distributor_address.panel{"ng-show" => "!Order.requireShipAddress()"} @@ -24,12 +27,10 @@ = f.fields_for :ship_address, @order.ship_address do |sa| #ship_address{"ng-if" => "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, - "ng-model" => "order.ship_address_same_as_billing" + %input{type: :checkbox, "ng-model" => "CheckoutFormState.ship_address_same_as_billing"} Shipping address same as billing address? - %div.visible{"ng-if" => "!order.ship_address_same_as_billing"} + %div.visible{"ng-if" => "!CheckoutFormState.ship_address_same_as_billing"} .row .large-12.columns = validated_input "Address", "order.ship_address.address1" 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 b4df9d4b9f..f5d5ac6a58 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -1,17 +1,19 @@ describe "CheckoutCtrl", -> ctrl = null scope = null - order = null + Order = null beforeEach -> module("Darkswarm") - order = { + Order = { submit: -> navigate: -> + order: + id: 1 } inject ($controller, $rootScope) -> scope = $rootScope.$new() - ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: order} + ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: Order} it "defaults the user accordion to visible", -> expect(scope.accordion.user).toEqual true @@ -20,6 +22,6 @@ describe "CheckoutCtrl", -> event = { preventDefault: -> } - spyOn(order, "submit") + spyOn(Order, "submit") scope.purchase(event) - expect(order.submit).toHaveBeenCalled() + expect(Order.submit).toHaveBeenCalled() diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 181d1acd31..4d3fd47696 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -2,6 +2,7 @@ describe 'Order service', -> Order = null orderData = null $httpBackend = null + CheckoutFormState = null beforeEach -> orderData = { @@ -26,29 +27,30 @@ describe 'Order service', -> inject ($injector, _$httpBackend_)-> $httpBackend = _$httpBackend_ Order = $injector.get("Order") + CheckoutFormState = $injector.get("CheckoutFormState") spyOn(Order, "navigate") # Stubbing out writes to window.location it "defaults the shipping method to the first", -> - expect(Order.shipping_method_id).toEqual 7 + expect(Order.order.shipping_method_id).toEqual 7 expect(Order.shippingMethod()).toEqual { require_ship_address : true, price : 0 } # This is now handled via localStorage defaults xit "defaults to 'same as billing' for address", -> - expect(Order.ship_address_same_as_billing).toEqual true + expect(Order.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 + Order.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 + Order.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 + Order.order.payment_method_id = 99 expect(Order.paymentMethod()).toEqual {test: "foo"} it "Posts the Order to the server", -> @@ -63,7 +65,7 @@ describe 'Order service', -> expect(Order.preprocess().ship_address).toBe(undefined) it "Munges the order attributes to clone ship address from bill address", -> - Order.ship_address_same_as_billing = false + CheckoutFormState.ship_address_same_as_billing = false expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address) - Order.ship_address_same_as_billing = true + CheckoutFormState.ship_address_same_as_billing = true expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address) From 2d9be9ea06958c579d765c07a0ab440064982f3e Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 11 Apr 2014 17:49:51 +1000 Subject: [PATCH 61/75] Fixing the scope issue --- .../darkswarm/controllers/checkout_controller.js.coffee | 7 ++++++- app/views/shop/checkout/_form.html.haml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index d7ce700b9c..585fa6dc3f 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,6 +1,12 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage, CheckoutFormState) -> + + # We put Order.order into the scope for convenience + # However, storage.bind replaces Order.order + # So we must put Order.order into the scope AFTER it's bound to localStorage $scope.Order = Order + storage.bind $scope, "Order.order", {storeName: "order_#{Order.order.id}"} $scope.order = Order.order + $scope.CheckoutFormState = CheckoutFormState #$scope.order = Order.order $scope.accordion = {} @@ -15,7 +21,6 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage, CheckoutFormState) storage.bind $scope, "accordion.payment" storage.bind $scope, "CheckoutFormState.ship_address_same_as_billing", { defaultValue: true} - storage.bind $scope, "order", {storeName: "order_#{$scope.order.id}"} $scope.purchase = (event)-> event.preventDefault() diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index a93b118ff1..a198213397 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -8,7 +8,7 @@ angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) -#%pre - -#{{ order | json }} + -#{{ Order.order == order }} .large-12.columns = render partial: "shop/checkout/details", locals: {f: f} From 19b8cee38ecf394be0c7a620d182da093000c723 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 11 Apr 2014 17:58:32 +1000 Subject: [PATCH 62/75] Patching up some more bugs, adding workflow --- .../controllers/checkout/shipping_controller.js.coffee | 3 +++ app/views/shop/checkout/_payment.html.haml | 3 ++- app/views/shop/checkout/_shipping.html.haml | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee index 82b435852b..99c621ecbc 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -1,3 +1,6 @@ Darkswarm.controller "ShippingCtrl", ($scope) -> angular.extend(this, new FieldsetMixin($scope)) $scope.name = "shipping" + $scope.nextPanel = "payment" + + window.tmp = $scope diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml index 78b0e9792f..c7ffd0c5dd 100644 --- a/app/views/shop/checkout/_payment.html.haml +++ b/app/views/shop/checkout/_payment.html.haml @@ -1,6 +1,7 @@ %fieldset#payment %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} - %accordion-group + %accordion-group{"is-open" => "accordion.payment", + "ng-class" => "{valid: payment.$valid}"} %accordion-heading .row .large-6.columns diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 7893f23ea3..f5636c81d1 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -1,5 +1,4 @@ %fieldset#shipping - %pre %ng-form{"ng-controller" => "ShippingCtrl", name: "shipping"} %accordion-group{"is-open" => "accordion.shipping", "ng-class" => "{valid: shipping.$valid}"} @@ -36,7 +35,7 @@ = validated_input "Address", "order.ship_address.address1" .row .large-12.columns - = validated_input "Address (contd.)", "order.ship_address.address2" + = validated_input "Address (contd.)", "order.ship_address.address2", required: false .row .large-6.columns = validated_input "City", "order.ship_address.city" @@ -56,3 +55,6 @@ .row .large-6.columns = validated_input "Phone", "order.ship_address.phone" + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next From 2b9b51de6090fcca1fbf95350c8314398ca9d5a6 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 16 Apr 2014 11:55:16 +1000 Subject: [PATCH 63/75] Don't cache things in window --- .../darkswarm/controllers/checkout/shipping_controller.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee index 99c621ecbc..a6a38329b4 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -3,4 +3,3 @@ Darkswarm.controller "ShippingCtrl", ($scope) -> $scope.name = "shipping" $scope.nextPanel = "payment" - window.tmp = $scope From d83367486b298c3fa17dbc7150c5e55501202cc1 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 16 Apr 2014 16:07:19 +1000 Subject: [PATCH 64/75] Adding icons to checkout, focusing on first input in each panel when selected --- app/assets/javascripts/darkswarm/all.js.coffee | 1 + .../javascripts/darkswarm/directives/focus.js.coffee | 11 +++++++++++ app/assets/stylesheets/darkswarm/checkout.css.sass | 7 +++++-- app/helpers/checkout_helper.rb | 10 +++++++--- app/views/shared/_validated_input.html.haml | 8 +++----- app/views/shop/checkout/_billing.html.haml | 4 +++- app/views/shop/checkout/_details.html.haml | 2 ++ app/views/shop/checkout/_payment.html.haml | 2 ++ app/views/shop/checkout/_shipping.html.haml | 2 ++ 9 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/focus.js.coffee diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index e94c975952..357e371974 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -16,6 +16,7 @@ #= require ./darkswarm #= require ./overrides #= require_tree ./mixins +#= require_tree ./directives #= require_tree . $ -> diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee new file mode 100644 index 0000000000..276e332578 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -0,0 +1,11 @@ +Darkswarm.directive "ofnFocus", -> + restrict: "A" + link: (scope, element, attrs) -> + scope.$watch attrs.ofnFocus, ((focus) -> + console.log focus + focus and element.focus() + return + ), true + + return + diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index 1d6819d57a..84e8eccf8a 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -5,5 +5,8 @@ checkout .button, table width: 100% - dd.valid - background: green + dd + i.fi-check, &.valid i.fi-x + display: none + &.valid i.fi-check + display: inline diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 6cf2aca961..147e240fce 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -12,10 +12,14 @@ module CheckoutHelper end def validated_input(name, path, args = {}) - defaults = { + attributes = { required: true, - type: :text + type: :text, + name: path, + "ng-model" => path, + "ng-class" => "{error: !fieldValid('#{path}')}" }.merge args - render partial: "shared/validated_input", locals: {name: name, path: path}.merge(defaults) + + render partial: "shared/validated_input", locals: {name: name, path: path, attributes: attributes} end end diff --git a/app/views/shared/_validated_input.html.haml b/app/views/shared/_validated_input.html.haml index 251c60b975..46331333fc 100644 --- a/app/views/shared/_validated_input.html.haml +++ b/app/views/shared/_validated_input.html.haml @@ -1,8 +1,6 @@ %label{for: path}= name -%input.medium.input-text{name: path, - "ng-model" => path, - required: required, - type: type, - "ng-class" => "{error: !fieldValid('#{path}')}"} + +%input.medium.input-text{attributes} + %small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} = "{{ fieldErrors('#{path}') }}" diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index fe7232dfbd..169e9d032b 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -9,10 +9,12 @@ .large-6.columns.text-right {{ order.bill_address.address1 }} {{ order.bill_address.city }} + %i.fi-x + %i.fi-check = f.fields_for :bill_address, @order.bill_address do |ba| .row .large-12.columns - = validated_input "Address", "order.bill_address.address1" + = validated_input "Address", "order.bill_address.address1", "ofn-focus" => "accordion['billing']" .row .large-12.columns = validated_input "Address (contd.)", "order.bill_address.address2", required: false diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 3f3a815452..de27e0dfb2 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -9,6 +9,8 @@ .large-6.columns.text-right {{ order.bill_address.firstname }} {{ order.bill_address.lastname }} + %i.fi-x + %i.fi-check .row .large-6.columns = validated_input('Email', 'order.email', type: :email) diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml index c7ffd0c5dd..7cb79f81b2 100644 --- a/app/views/shop/checkout/_payment.html.haml +++ b/app/views/shop/checkout/_payment.html.haml @@ -8,6 +8,8 @@ Payment Details .large-6.columns.text-right {{ Order.paymentMethod().name }} + %i.fi-x + %i.fi-check - 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 f5636c81d1..34f543ddd7 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -8,6 +8,8 @@ Shipping .large-6.columns.text-right {{ Order.shippingMethod().name }} + %i.fi-x + %i.fi-check - for ship_method, i in current_distributor.shipping_methods.uniq .row .large-12.columns From 20e13927362a4f5f99ae15c9e1d4545caafca21d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 16 Apr 2014 16:13:04 +1000 Subject: [PATCH 65/75] Focusing appropriate fields as we move through the form --- app/views/shop/checkout/_details.html.haml | 2 +- app/views/shop/checkout/_shipping.html.haml | 2 +- app/views/shop/checkout/_summary.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index de27e0dfb2..423ca7248f 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -13,7 +13,7 @@ %i.fi-check .row .large-6.columns - = validated_input('Email', 'order.email', type: :email) + = validated_input 'Email', 'order.email', type: :email, "ofn-focus" => "accordion['details']" = f.fields_for :bill_address, @order.bill_address do |ba| .large-6.columns diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 34f543ddd7..ebc09ed2a2 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -34,7 +34,7 @@ %div.visible{"ng-if" => "!CheckoutFormState.ship_address_same_as_billing"} .row .large-12.columns - = validated_input "Address", "order.ship_address.address1" + = validated_input "Address", "order.ship_address.address1", "ofn-focus" => "accordion['shipping']" .row .large-12.columns = validated_input "Address (contd.)", "order.ship_address.address2", required: false diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 730263a249..c2a0a34270 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", "ng-disabled" => "checkout.$invalid" + = f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} Back to Cart From a7c2849e6bdfb7d966cc1e2e0a484132893b7c34 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Wed, 16 Apr 2014 16:24:03 +1000 Subject: [PATCH 66/75] Moving icons around, adding a useful comment --- .../darkswarm/services/checkout_form_state.js.coffee | 2 ++ app/views/shop/checkout/_billing.html.haml | 4 ++-- app/views/shop/checkout/_details.html.haml | 4 ++-- app/views/shop/checkout/_payment.html.haml | 4 ++-- app/views/shop/checkout/_shipping.html.haml | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee index b2f16aea7b..40c1dd6d61 100644 --- a/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee @@ -1,2 +1,4 @@ Darkswarm.factory 'CheckoutFormState', ()-> + # This class only exists to encapsulate a single field: checkout_state_same_as_billing + # So we can cleanly access it from the Order service as well as the scope new class CheckoutFormState diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml index 169e9d032b..6872bcaabc 100644 --- a/app/views/shop/checkout/_billing.html.haml +++ b/app/views/shop/checkout/_billing.html.haml @@ -6,11 +6,11 @@ .row .large-6.columns Billing + %i.fi-x + %i.fi-check .large-6.columns.text-right {{ order.bill_address.address1 }} {{ order.bill_address.city }} - %i.fi-x - %i.fi-check = f.fields_for :bill_address, @order.bill_address do |ba| .row .large-12.columns diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml index 423ca7248f..8b626f924f 100644 --- a/app/views/shop/checkout/_details.html.haml +++ b/app/views/shop/checkout/_details.html.haml @@ -6,11 +6,11 @@ .row .large-6.columns Customer Details + %i.fi-x + %i.fi-check .large-6.columns.text-right {{ order.bill_address.firstname }} {{ order.bill_address.lastname }} - %i.fi-x - %i.fi-check .row .large-6.columns = validated_input 'Email', 'order.email', type: :email, "ofn-focus" => "accordion['details']" diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml index 7cb79f81b2..6762924d36 100644 --- a/app/views/shop/checkout/_payment.html.haml +++ b/app/views/shop/checkout/_payment.html.haml @@ -6,10 +6,10 @@ .row .large-6.columns Payment Details - .large-6.columns.text-right - {{ Order.paymentMethod().name }} %i.fi-x %i.fi-check + .large-6.columns.text-right + {{ 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 ebc09ed2a2..7edcd02aef 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -6,10 +6,10 @@ .row .large-6.columns Shipping - .large-6.columns.text-right - {{ Order.shippingMethod().name }} %i.fi-x %i.fi-check + .large-6.columns.text-right + {{ Order.shippingMethod().name }} - for ship_method, i in current_distributor.shipping_methods.uniq .row .large-12.columns From f2a048bf6e0b5ed79d02632c054cdc24c796c8a9 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 10:50:49 +1000 Subject: [PATCH 67/75] Adding another autofocus --- app/views/shop/checkout/_shipping.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml index 7edcd02aef..d70b717525 100644 --- a/app/views/shop/checkout/_shipping.html.haml +++ b/app/views/shop/checkout/_shipping.html.haml @@ -59,4 +59,4 @@ = validated_input "Phone", "order.ship_address.phone" .row .large-12.columns.text-right - %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)", "ofn-focus" => "accordion['shipping']"} Next From 3b440ed027ec7645805a7742d7712964a87c301e Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 11:47:59 +1000 Subject: [PATCH 68/75] Minor JS tweaks --- .../darkswarm/controllers/account_sidebar_controller.js.coffee | 1 - app/assets/javascripts/darkswarm/directives/focus.js.coffee | 1 - app/assets/javascripts/darkswarm/services/order.js.coffee | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) 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 b7180fab49..2ce5419400 100644 --- a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee @@ -9,7 +9,6 @@ window.AccountSidebarCtrl = Darkswarm.controller "AccountSidebarCtrl", ($scope, Navigation.navigate($scope.path) $scope.emptyCart = (href, ev)-> - console.log href if $(ev.delegateTarget).hasClass "empties-cart" location.href = href if confirm "Changing your Hub will clear your cart." else diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee index 276e332578..b9cae51e5e 100644 --- a/app/assets/javascripts/darkswarm/directives/focus.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -2,7 +2,6 @@ Darkswarm.directive "ofnFocus", -> restrict: "A" link: (scope, element, attrs) -> scope.$watch attrs.ofnFocus, ((focus) -> - console.log focus focus and element.focus() return ), true diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 2da4983b93..450d14fe54 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -5,7 +5,7 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState) constructor: -> @order = order # Default to first shipping method if none selected - #@order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) + @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) navigate: (path)-> window.location.pathname = path From c6395a686ad2b2f997af96e25f926fd43965ef3b Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 15:08:01 +1000 Subject: [PATCH 69/75] Adding flash notifications --- app/assets/javascripts/darkswarm/all.js.coffee | 1 + .../javascripts/darkswarm/darkswarm.js.coffee | 2 +- .../darkswarm/directives/flash.js.coffee | 15 +++++++++++++++ .../darkswarm/directives/focus.js.coffee | 1 - .../darkswarm/services/order.js.coffee | 10 ++++++---- app/controllers/shop/checkout_controller.rb | 2 +- app/views/layouts/darkswarm.html.haml | 1 + spec/controllers/shop/checkout_controller_spec.rb | 9 ++++++++- spec/javascripts/application_spec.js | 2 +- .../unit/darkswarm/services/order_spec.js.coffee | 15 +++++++++++++++ vendor/assets/javascripts/angular-flash.min.js | 6 ++++++ 11 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/directives/flash.js.coffee create mode 100644 vendor/assets/javascripts/angular-flash.min.js diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 357e371974..65fa9107a9 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -10,6 +10,7 @@ #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js +#= require angular-flash.min.js # #= require ../shared/jquery.timeago #= require foundation diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index ac9eefe0df..71d99be03b 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', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll']).config ($httpProvider, $tooltipProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll', 'angular-flash.service']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' diff --git a/app/assets/javascripts/darkswarm/directives/flash.js.coffee b/app/assets/javascripts/darkswarm/directives/flash.js.coffee new file mode 100644 index 0000000000..86eb2a05a2 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/flash.js.coffee @@ -0,0 +1,15 @@ +Darkswarm.directive "ofnFlash", (flash, $timeout)-> + scope: {} + restrict: 'AE' + template: "{{flash.message}}" + link: ($scope, element, attr) -> + $scope.flashes = [] + show = (message, type)-> + if message + $scope.flashes.push({message: message, type: type}) + $timeout($scope.delete, 5000) + + $scope.delete = -> + $scope.flashes.shift() + + flash.subscribe(show) diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee index b9cae51e5e..c481702d6c 100644 --- a/app/assets/javascripts/darkswarm/directives/focus.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -7,4 +7,3 @@ Darkswarm.directive "ofnFocus", -> ), true return - diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee index 450d14fe54..c3bebdf143 100644 --- a/app/assets/javascripts/darkswarm/services/order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState)-> +Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState, flash)-> new class Order errors: {} @@ -13,9 +13,11 @@ Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState) submit: -> $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> @navigate(data.path) - .error (errors, status)=> - @errors = errors - + .error (response, status)=> + @errors = response.errors + flash.error = response.flash?.error + flash.success = response.flash?.notice + # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> munged_order = {} diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb index 0ffe90a91f..8f28645ab8 100644 --- a/app/controllers/shop/checkout_controller.rb +++ b/app/controllers/shop/checkout_controller.rb @@ -53,7 +53,7 @@ class Shop::CheckoutController < Spree::CheckoutController render :edit end format.js do - render json: @order.errors.to_json, status: 400 + render json: {errors: @order.errors, flash: flash.to_hash}.to_json, status: 400 end end end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 51aaca837d..400d4a33c3 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -15,6 +15,7 @@ %body.off-canvas{"ng-app" => "Darkswarm"} = render partial: "shared/menu" = display_flash_messages + %ofn-flash = render "shared/sidebar" diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index d5c5ec60a5..9086fe0953 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -88,7 +88,14 @@ describe Shop::CheckoutController do it "returns errors" do xhr :post, :update, order: {}, use_route: :spree response.status.should == 400 - response.body.should == assigns[:order].errors.to_json + response.body.should == {errors: assigns[:order].errors, flash: []}.to_json + end + + it "returns flash" do + order.stub(:update_attributes).and_return true + order.stub(:next).and_return false + xhr :post, :update, order: {}, use_route: :spree + response.body.should == {errors: assigns[:order].errors, flash: {error: "Payment could not be processed, please check the details you entered"}}.to_json end it "returns order confirmation url on success" do diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index cd070d79e9..615e29535e 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -3,4 +3,4 @@ //= require angular-animate //= require angular-mocks //= require angular-cookies -// +//= require angular-flash.min.js diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee index 4d3fd47696..f78cec582b 100644 --- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -3,6 +3,7 @@ describe 'Order service', -> orderData = null $httpBackend = null CheckoutFormState = null + flash = null beforeEach -> orderData = { @@ -27,6 +28,7 @@ describe 'Order service', -> inject ($injector, _$httpBackend_)-> $httpBackend = _$httpBackend_ Order = $injector.get("Order") + flash = $injector.get("flash") CheckoutFormState = $injector.get("CheckoutFormState") spyOn(Order, "navigate") # Stubbing out writes to window.location @@ -58,6 +60,19 @@ describe 'Order service', -> Order.submit() $httpBackend.flush() + it "sends flash messages to the flash service", -> + $httpBackend.expectPUT("/shop/checkout").respond 400, {flash: {error: "frogs"}} + Order.submit() + $httpBackend.flush() + expect(flash.error).toEqual "frogs" + + it "puts errors into the scope", -> + $httpBackend.expectPUT("/shop/checkout").respond 400, {errors: {error: "frogs"}} + Order.submit() + $httpBackend.flush() + expect(Order.errors).toEqual {error: "frogs"} + + it "Munges the order attributes to add _attributes as Rails needs", -> expect(Order.preprocess().bill_address_attributes).not.toBe(undefined) expect(Order.preprocess().bill_address).toBe(undefined) diff --git a/vendor/assets/javascripts/angular-flash.min.js b/vendor/assets/javascripts/angular-flash.min.js new file mode 100644 index 0000000000..5e46b88c87 --- /dev/null +++ b/vendor/assets/javascripts/angular-flash.min.js @@ -0,0 +1,6 @@ +/**! + * @license angular-flash v0.1.13 + * Copyright (c) 2013 William L. Bunselmeyer. https://github.com/wmluke/angular-flash + * License: MIT + */ +!function(){"use strict";var a=0,b=function(c){function d(a,b){angular.forEach(j.subscribers,function(c){var d=!c.type||c.type===a,e=!j.id&&!c.id||c.id===j.id;d&&e&&c.cb(b,a)})}var e,f,g,h,i,j=angular.extend({id:null,subscribers:{},classnames:{error:[],warn:[],info:[],success:[]}},c),k=this;this.clean=function(){e=null,f=null,g=null,h=null,i=null},this.subscribe=function(b,c,d){return a+=1,j.subscribers[a]={cb:b,type:c,id:d},a},this.unsubscribe=function(a){delete j.subscribers[a]},this.to=function(a){var c=angular.copy(j);return c.id=a,new b(c)},Object.defineProperty(this,"success",{get:function(){return e},set:function(a){e=a,i="success",d(i,a)}}),Object.defineProperty(this,"info",{get:function(){return f},set:function(a){f=a,i="info",d(i,a)}}),Object.defineProperty(this,"warn",{get:function(){return g},set:function(a){g=a,i="warn",d(i,a)}}),Object.defineProperty(this,"error",{get:function(){return h},set:function(a){h=a,i="error",d(i,a)}}),Object.defineProperty(this,"type",{get:function(){return i}}),Object.defineProperty(this,"message",{get:function(){return i?k[i]:null}}),Object.defineProperty(this,"classnames",{get:function(){return j.classnames}}),Object.defineProperty(this,"id",{get:function(){return j.id}})};angular.module("angular-flash.service",[]).provider("flash",function(){var a=this;this.errorClassnames=["alert-error"],this.warnClassnames=["alert-warn"],this.infoClassnames=["alert-info"],this.successClassnames=["alert-success"],this.$get=function(){return new b({classnames:{error:a.errorClassnames,warn:a.warnClassnames,info:a.infoClassnames,success:a.successClassnames}})}})}(),function(){"use strict";function a(a){return(null===a||void 0===a)&&(a=""),/^\s*$/.test(a)}function b(b,c){return{scope:!0,link:function(d,e,f){function g(){var a=[].concat(b.classnames.error,b.classnames.warn,b.classnames.info,b.classnames.success);angular.forEach(a,function(a){e.removeClass(a)})}function h(h,j){if(i&&c.cancel(i),d.flash.type=j,d.flash.message=h,g(),angular.forEach(b.classnames[j],function(a){e.addClass(a)}),a(f.activeClass)||e.addClass(f.activeClass),!h)return void d.hide();var k=Number(f.duration||5e3);k>0&&(i=c(d.hide,k))}var i,j;d.flash={},d.hide=function(){g(),a(f.activeClass)||e.removeClass(f.activeClass)},d.$on("$destroy",function(){b.clean(),b.unsubscribe(j)}),j=b.subscribe(h,f.flashAlert,f.id),f.flashAlert&&b[f.flashAlert]&&h(b[f.flashAlert],f.flashAlert),!f.flashAlert&&b.message&&h(b.message,b.type)}}}angular.module("angular-flash.flash-alert-directive",["angular-flash.service"]).directive("flashAlert",["flash","$timeout",b])}(); \ No newline at end of file From 202b45fc5fbc94fda71363a369886f66953177b5 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 15:15:14 +1000 Subject: [PATCH 70/75] Aligning text left in Order Summary --- app/assets/stylesheets/darkswarm/checkout.css.sass | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index 84e8eccf8a..4b8065819a 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -10,3 +10,6 @@ checkout display: none &.valid i.fi-check display: inline + + orderdetails table tr th + text-align: left From e2d3207a51c81dc676cc4a8318bb509204eade9d Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 15:34:57 +1000 Subject: [PATCH 71/75] Namespacing accordion state by ID --- .../controllers/checkout_controller.js.coffee | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index 585fa6dc3f..7bf69de294 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -9,16 +9,18 @@ Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage, CheckoutFormState) $scope.CheckoutFormState = CheckoutFormState #$scope.order = Order.order - $scope.accordion = {} + $scope.accordion = {user: true} $scope.show = (name)-> $scope.accordion[name] = true - storage.bind $scope, "accordion.user", { defaultValue: true} - storage.bind $scope, "accordion.details" - storage.bind $scope, "accordion.billing" - storage.bind $scope, "accordion.shipping" - storage.bind $scope, "accordion.payment" + storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}"} + console.log "===============================" + console.log "Order.order.id" + + #storage.bind $scope, "accordion.billing", {storeName: "billing_#{Order.order.id}"} + #storage.bind $scope, "accordion.shipping", {storeName: "shipping_#{Order.order.id}"} + #storage.bind $scope, "accordion.payment", {storeName: "payment_#{Order.order.id}"} storage.bind $scope, "CheckoutFormState.ship_address_same_as_billing", { defaultValue: true} From a5a3f9fe43db6c22ba742a84622ccb22b7caaa84 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Thu, 17 Apr 2014 17:22:30 +1000 Subject: [PATCH 72/75] Fixing up some minor spec bugs --- app/helpers/checkout_helper.rb | 1 + .../consumer/shopping/checkout_spec.rb | 66 +++++-------------- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 147e240fce..32e25bdfc5 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -16,6 +16,7 @@ module CheckoutHelper required: true, type: :text, name: path, + id: path, "ng-model" => path, "ng-class" => "{error: !fieldValid('#{path}')}" }.merge args diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 8343952170..c1fe87a572 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -40,54 +40,21 @@ feature "As a consumer I want to check out my cart", js: true 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 + toggle_accordion "Shipping" 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 + toggle_accordion "Shipping" 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) - 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" end end end @@ -101,10 +68,6 @@ 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 @@ -114,25 +77,19 @@ feature "As a consumer I want to check out my cart", js: true do end describe "Purchasing" do - it "re-renders with errors when we submit the incomplete form" 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 - - it "renders errors on the shipping method where appropriate" - it "takes us to the order confirmation page when we submit a complete form" do + toggle_accordion "Shipping" choose sm2.name + toggle_accordion "Payment Details" choose pm1.name + toggle_accordion "Customer Details" 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 "Email", with: "test@test.com" fill_in "Phone", with: "0468363090" end + toggle_accordion "Billing" within "#billing" do fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" @@ -141,18 +98,23 @@ feature "As a consumer I want to check out my cart", js: true do fill_in "Postcode", with: "3066" end click_button "Purchase" + sleep 10 page.should have_content "Your order has been processed successfully" end it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + toggle_accordion "Shipping" choose sm1.name + toggle_accordion "Payment Details" choose pm1.name + toggle_accordion "Customer Details" 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 "Email", with: "test@test.com" fill_in "Phone", with: "0468363090" end + toggle_accordion "Billing" within "#billing" do fill_in "City", with: "Melbourne" fill_in "Postcode", with: "3066" @@ -160,8 +122,10 @@ feature "As a consumer I want to check out my cart", js: true do select "Australia", from: "Country" select "Victoria", from: "State" end + toggle_accordion "Shipping" check "Shipping address same as billing address?" click_button "Purchase" + sleep 10 page.should have_content "Your order has been processed successfully" end end From c0228e0cc39334ac71ff84f943d091ded3392282 Mon Sep 17 00:00:00 2001 From: Will Marshall Date: Fri, 18 Apr 2014 11:37:19 +1000 Subject: [PATCH 73/75] Patching the admin JS --- app/assets/javascripts/admin/all.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 6e569ed67b..4cce0bdcea 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -9,8 +9,8 @@ //= require jquery_ujs //= require jquery-ui //= require shared/jquery-ui-timepicker-addon -//= require shared/angular -//= require shared/angular-resource +//= require angular +//= require angular-resource //= require admin/spree_core //= require admin/spree_auth //= require admin/spree_promo From 353b26c3dfaba5e18211c29efc92a24a67b8a2aa Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Apr 2014 12:02:11 +1000 Subject: [PATCH 74/75] Hack fix for our API controllers not being able to access Spree's Rabl Responder --- app/controllers/api/enterprises_controller.rb | 2 +- app/controllers/api/order_cycles_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index 102c237c95..dd8f47df11 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -4,7 +4,7 @@ module Api def managed @enterprises = Enterprise.ransack(params[:q]).result.managed_by(current_api_user) - respond_with(@enterprises) + render params[:template] || :bulk_index end end end diff --git a/app/controllers/api/order_cycles_controller.rb b/app/controllers/api/order_cycles_controller.rb index 89c815ea3c..3807fbebb8 100644 --- a/app/controllers/api/order_cycles_controller.rb +++ b/app/controllers/api/order_cycles_controller.rb @@ -3,7 +3,7 @@ module Api respond_to :json def managed @order_cycles = OrderCycle.ransack(params[:q]).result.managed_by(current_api_user) - render :bulk_index + render params[:template] || :bulk_index end end end From 669ec0eee72d20a2c84082882d6f450d3c6f8d50 Mon Sep 17 00:00:00 2001 From: Rob H Date: Wed, 23 Apr 2014 12:02:27 +1000 Subject: [PATCH 75/75] Fix failing API specs --- spec/features/admin/bulk_order_management_spec.rb | 4 ++-- spec/features/admin/bulk_product_update_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 9b5ac1dabe..1a227bc6bd 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -507,7 +507,7 @@ feature %q{ before :each do visit '/admin/orders/bulk_management' within "tr#li_#{li3.id}" do - click_link li3.variant.options_text + find("a", text: li3.product.name + ": " + li3.variant.options_text).click end end @@ -542,7 +542,7 @@ feature %q{ context "clicking 'Clear' in group buy box" do before :each do - click_link 'Clear' + find("a", text: "Clear").click end it "shows all products and clears group buy box" do diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 3de9b6f1e1..9403c8cac9 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -237,7 +237,7 @@ feature %q{ visit '/admin/products/bulk_edit' - click_link 'New Product' + find("a", text: "New Product").click page.should have_content 'NEW PRODUCT' @@ -783,7 +783,7 @@ feature %q{ select '25', :from => 'perPage' page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page1product" }.should == true - click_link "2" + find("a", text: "2").click page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page2product" }.should == true end @@ -795,7 +795,7 @@ feature %q{ visit '/admin/products/bulk_edit' select '25', :from => 'perPage' - click_link "3" + find("a", text: "3").click select '50', :from => 'perPage' page.first("div.pagenav span.page.current").should have_text "2" page.all("input[name='product_name']", :visible => true).length.should == 1 @@ -869,7 +869,7 @@ feature %q{ describe "clicking the 'Remove Filter' link" do before(:each) do - click_link "Remove Filter" + find("a", text: "Remove Filter").click end it "removes the filter from the list of applied filters" do