Merge branch 'master' into laura_and_will

Conflicts:
	app/views/shopping_shared/_contact.html.haml
This commit is contained in:
Will Marshall
2014-06-04 17:24:30 +10:00
29 changed files with 282 additions and 95 deletions

View File

@@ -9,8 +9,8 @@ Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout) ->
$scope.show = (name)->
$scope.accordion[name] = true
$timeout =>
if $scope.checkout.$valid
for k, v of $scope.accordion
$scope.accordion[k] = false
#$timeout =>
#if $scope.checkout.$valid
#for k, v of $scope.accordion
#$scope.accordion[k] = false

View File

@@ -1,4 +1,4 @@
Darkswarm.controller "BillingCtrl", ($scope) ->
Darkswarm.controller "BillingCtrl", ($scope, $timeout) ->
angular.extend(this, new FieldsetMixin($scope))
$scope.name = "billing"
$scope.nextPanel = "shipping"
@@ -8,3 +8,4 @@ Darkswarm.controller "BillingCtrl", ($scope) ->
$scope.order.bill_address.city,
$scope.order.bill_address.zipcode]
#$timeout $scope.onTimeout

View File

@@ -1,13 +1,12 @@
Darkswarm.controller "CheckoutCtrl", ($scope, storage, CheckoutFormState, Order, CurrentUser) ->
Darkswarm.controller "CheckoutCtrl", ($scope, storage, Order, CurrentUser) ->
$scope.Order = Order
storage.bind $scope, "Order.order", {storeName: "order_#{Order.order.id}#{Order.order.user_id}"}
Order.bindFieldsToLocalStorage($scope)
$scope.order = Order.order # Ordering is important
$scope.secrets = Order.secrets
$scope.enabled = if CurrentUser then true else false
$scope.purchase = (event)->
event.preventDefault()
$scope.Order.submit()
$scope.CheckoutFormState = CheckoutFormState
storage.bind $scope, "CheckoutFormState.ship_address_same_as_billing", { defaultValue: true}

View File

@@ -1,6 +1,5 @@
Darkswarm.controller "DetailsCtrl", ($scope) ->
Darkswarm.controller "DetailsCtrl", ($scope, $timeout) ->
angular.extend(this, new FieldsetMixin($scope))
$scope.name = "details"
$scope.nextPanel = "billing"
@@ -12,3 +11,5 @@ Darkswarm.controller "DetailsCtrl", ($scope) ->
$scope.fullName = ->
[$scope.order.bill_address.firstname ? null,
$scope.order.bill_address.lastname ? null].join(" ").trim()
$timeout $scope.onTimeout

View File

@@ -1,3 +1,9 @@
Darkswarm.controller "PaymentCtrl", ($scope) ->
Darkswarm.controller "PaymentCtrl", ($scope, $timeout) ->
angular.extend(this, new FieldsetMixin($scope))
$scope.name = "payment"
$scope.months = {1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"}
$scope.years = [moment().year()..(moment().year()+15)]
$scope.secrets.card_month = "1"
$scope.secrets.card_year = moment().year()
$timeout $scope.onTimeout

View File

@@ -1,5 +1,6 @@
Darkswarm.controller "ShippingCtrl", ($scope) ->
Darkswarm.controller "ShippingCtrl", ($scope, $timeout) ->
angular.extend(this, new FieldsetMixin($scope))
$scope.name = "shipping"
$scope.nextPanel = "payment"
$timeout $scope.onTimeout

View File

@@ -1,12 +1,20 @@
Darkswarm.directive "ofnFlash", (flash, $timeout)->
typePairings =
info: "standard"
error: "alert"
success: "success"
scope: {}
restrict: 'AE'
template: "<alert ng-repeat='flash in flashes' type='flash.type'>{{flash.message}}</alert>"
templateUrl: "flash.html"
controller: ($scope)->
$scope.closeAlert = (index)->
$scope.flashes.splice(index, 1)
link: ($scope, element, attr) ->
$scope.flashes = []
show = (message, type)->
show = (message, type)=>
if message
$scope.flashes.push({message: message, type: type})
$scope.flashes.push({message: message, type: typePairings[type]})
$timeout($scope.delete, 5000)
$scope.delete = ->

View File

@@ -1,8 +1,12 @@
window.FieldsetMixin = ($scope)->
$scope.next = (event)->
event.preventDefault()
$scope.next = (event = false)->
event.preventDefault() if event
$scope.show $scope.nextPanel
$scope.onTimeout = ->
if $scope[$scope.name].$valid
$scope.next()
$scope.valid = ->
$scope.form().$valid

View File

@@ -1,3 +0,0 @@
Darkswarm.factory 'CheckoutFormState', ()->
# Just a singleton place to store data about the form state
new class CheckoutFormState

View File

@@ -2,6 +2,5 @@ Darkswarm.factory 'CurrentOrder', (currentOrder) ->
new class CurrentOrder
constructor: ->
@[k] = v for k, v of currentOrder
empty: =>
@line_items.length == 0

View File

@@ -0,0 +1,9 @@
Darkswarm.factory 'Flash', (flash)->
new class Flash
loadFlash: (rails_flash)->
for type, message of rails_flash
switch type
when "notice"
flash.info = message
else
flash[type] = message

View File

@@ -1,9 +1,23 @@
Darkswarm.factory 'Order', ($resource, order, $http, CheckoutFormState, flash, Navigation)->
Darkswarm.factory 'Order', ($resource, order, $http, flash, Navigation, storage)->
new class Order
errors: {}
secrets: {}
order: order
ship_address_same_as_billing: true
constructor: ->
@order = order
#Whitelist of fields from Order.order to bind into localStorage
fieldsToBind: ["bill_address", "email", "payment_method_id", "shipping_method_id", "ship_address"]
# Bind all the fields from fieldsToBind, + anything on the Order class
bindFieldsToLocalStorage: (scope)=>
prefix = "order_#{@order.id}#{@order.user_id}"
for field in @fieldsToBind
storage.bind scope, "Order.order.#{field}",
storeName: "#{prefix}_#{field}"
storage.bind scope, "Order.ship_address_same_as_billing",
storeName: "#{prefix}_sameasbilling"
defaultValue: true
submit: ->
$http.put('/checkout', {order: @preprocess()}).success (data, status)=>
@@ -24,12 +38,25 @@ Darkswarm.factory 'Order', ($resource, order, $http, CheckoutFormState, flash, N
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 CheckoutFormState.ship_address_same_as_billing
if @ship_address_same_as_billing
munged_order.ship_address_attributes = munged_order.bill_address_attributes
if @paymentMethod()?.method_type == 'gateway'
angular.extend munged_order.payments_attributes[0], {
source_attributes:
number: @secrets.card_number
month: @secrets.card_month
year: @secrets.card_year
verification_value: @secrets.card_verification_value
first_name: @order.bill_address.firstname
last_name: @order.bill_address.lastname
}
munged_order
shippingMethod: ->
@@ -39,7 +66,7 @@ Darkswarm.factory 'Order', ($resource, order, $http, CheckoutFormState, flash, N
@shippingMethod()?.require_ship_address
shippingPrice: ->
@shippingMethod()?.price
@shippingMethod()?.price || 0.0
paymentMethod: ->
@order.payment_methods[@order.payment_method_id]

View File

@@ -0,0 +1,2 @@
%alert{close: "closeAlert($index)", "ng-repeat" => "flash in flashes", type: "flash.type"}
{{ flash.message }}

View File

@@ -15,7 +15,6 @@ class CheckoutController < Spree::CheckoutController
end
def update
if @order.update_attributes(params[:order])
fire_event('spree.checkout.update')
while @order.state != "complete"
@@ -26,7 +25,7 @@ class CheckoutController < Spree::CheckoutController
if @order.next
state_callback(:after)
else
unless @order.errors.empty?
if @order.errors.present?
flash[:error] = @order.errors.full_messages.to_sentence
else
flash[:error] = t(:payment_processing_failed)

View File

@@ -149,6 +149,16 @@ class Enterprise < ActiveRecord::Base
self.relatives.is_distributor
end
def website
strip_url read_attribute(:website)
end
def facebook
strip_url read_attribute(:facebook)
end
def linkedin
strip_url read_attribute(:linkedin)
end
def suppliers
self.relatives.is_primary_producer
end
@@ -180,6 +190,10 @@ class Enterprise < ActiveRecord::Base
private
def strip_url(url)
url.andand.sub /(https?:\/\/)?(www\.)?/, ''
end
def initialize_country
self.address ||= Spree::Address.new
self.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) if self.address.new_record?

View File

@@ -1,3 +1,6 @@
#NOTE: when adding new fields for user input, it may want to be cached in localStorage
# If so, make sure to add it to Order.attributes_to_cache
object current_order
attributes :id, :email, :shipping_method_id, :user_id
@@ -18,7 +21,7 @@ child current_order.ship_address => :ship_address do
end
node :shipping_methods do
Hash[current_order.available_shipping_methods("front_end").collect { |method|
Hash[current_distributor.shipping_methods.uniq.collect { |method|
[method.id, {
require_ship_address: method.require_ship_address,
price: method.compute_amount(current_order).to_f,
@@ -30,7 +33,8 @@ end
node :payment_methods do
Hash[current_order.available_payment_methods.collect {
|method| [method.id, {
name: method.name
name: method.name,
method_type: method.method_type
}]
}]
end

View File

@@ -42,10 +42,10 @@
= f.fields_for :ship_address, @order.ship_address do |sa|
#ship_address{"ng-if" => "Order.requireShipAddress()"}
%label
%input{type: :checkbox, "ng-model" => "CheckoutFormState.ship_address_same_as_billing"}
%input{type: :checkbox, "ng-model" => "Order.ship_address_same_as_billing"}
Shipping address same as billing address?
%div.visible{"ng-if" => "!CheckoutFormState.ship_address_same_as_billing"}
%div.visible{"ng-if" => "!Order.ship_address_same_as_billing"}
.row
.small-12.columns
= validated_input "Address", "order.ship_address.address1", "ofn-focus" => "accordion['shipping']"

View File

@@ -1,4 +1,4 @@
attributes :name, :id, :description, :long_description
attributes :id, :name, :description, :long_description, :website, :instagram, :facebook, :linkedin, :twitter
node :promo_image do |producer|
producer.promo_image.url

View File

@@ -17,13 +17,18 @@
"ng-repeat" => "product in data.products | filter:query | orderBy:ordering.order | limitTo: limit track by product.id"}
%div
= render partial: "shop/products/summary"
%div{"bo-if" => "hasVariants"}
= render partial: "shop/products/variants"
.variants.row{"bo-if" => "!hasVariants"}
= render partial: "shop/products/master"
%product{"ng-show" => "data.loading"}
.row.summary
.small-12.columns.text-center
Loading products
.row
.small-12.columns
%input.button.primary.right.add_to_cart{type: :submit, value: "Add to Cart"}

View File

@@ -2,7 +2,8 @@
.row
.small-12.large-9.columns
%p= current_distributor.long_description.andand.html_safe
.small-12.large-3.columns
/ Hide image until image styles are working correctly:
/ %img.about.right{src: current_distributor.promo_image.url(:large)}
- if current_distributor.promo_image.exists?
.small-12.large-3.columns
%img.about.right{src: current_distributor.promo_image.url(:large)}

View File

@@ -19,7 +19,7 @@
%ul.small-block-grid-1.large-block-grid-2{bindonce: true}
- unless current_distributor.website.blank?
%li
%a{href: current_distributor.website, target: "_blank" }
%a{href: "http://#{current_distributor.website}", target: "_blank" }
%i.ofn-i_049-web
= current_distributor.website
@@ -32,24 +32,24 @@
- unless current_distributor.twitter.blank?
%li
%a{href: current_distributor.twitter, target: "_blank" }
%a{href: "http://twitter.com/#{current_distributor.twitter}", target: "_blank" }
%i.ofn-i_041-twitter
= current_distributor.twitter
- unless current_distributor.facebook.blank?
%li
%a{href: current_distributor.facebook, target: "_blank" }
%a{href: "http://#{current_distributor.facebook}", target: "_blank" }
%i.ofn-i_044-facebook
= current_distributor.facebook
- unless current_distributor.linkedin.blank?
%li
%a{href: current_distributor.linkedin, target: "_blank" }
%a{href: "http://#{current_distributor.linkedin}", target: "_blank" }
%i.ofn-i_042-linkedin
= current_distributor.linkedin
- unless current_distributor.instagram.blank?
%li
%a{href: current_distributor.instagram, target: "_blank" }
%a{href: "http://instagram.com.#{current_distributor.instagram}", target: "_blank" }
%i.ofn-i_043-instagram
= current_distributor.instagram

View File

@@ -1,7 +1,8 @@
%navigation
%distributor.details.row
#distributor_title
%img.left{src: current_distributor.logo.url(:thumb)}
- if current_distributor.logo.exists?
%img.left{src: current_distributor.logo.url(:thumb)}
%h3
= current_distributor.name
%location= current_distributor.address.city

View File

@@ -0,0 +1,8 @@
= validated_input "Card Number", "secrets.card_number", required: true, maxlength: 19, autocomplete: "off"
%label{for: "secrets.card_month"} Expiry Date
%select{"ng-model" => "secrets.card_month", "ng-options" => "number as name for (number, name) in months", name: "secrets.card_month", required: true}
%select{"ng-model" => "secrets.card_year", "ng-options" => "year for year in years", name: "secrets.card_year", required: true}
= validated_input "CVV", "secrets.card_verification_value", required: true

View File

@@ -6,7 +6,9 @@ class AddPrimaryTaxonToProducts < ActiveRecord::Migration
add_foreign_key :spree_products, :spree_taxons, column: :primary_taxon_id
Spree::Product.all.each do |p|
p.update_column :primary_taxon_id, (p.taxons.first || Spree::Taxon.first)
primary_taxon = p.taxons.where('spree_taxons.name != ?', 'specials').first
first_taxon = Spree::Taxonomy.find_by_name('Products').andand.root || Spree::Taxon.first
p.update_column :primary_taxon_id, (primary_taxon || first_taxon)
end
change_column :spree_products, :primary_taxon_id, :integer, null: false

View File

@@ -96,37 +96,57 @@ feature "As a consumer I want to check out my cart", js: true do
end
place_order
page.should have_content "Your order has been processed successfully", wait: 10
page.should have_content "Your order has been processed successfully"
ActionMailer::Base.deliveries.length.should == 1
email = ActionMailer::Base.deliveries.last
site_name = Spree::Config[:site_name]
email.subject.should include "#{site_name} Order Confirmation"
end
it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do
toggle_shipping
choose sm1.name
toggle_payment
choose pm1.name
toggle_details
within "#details" do
fill_in "First Name", with: "Will"
fill_in "Last Name", with: "Marshall"
fill_in "Email", with: "test@test.com"
fill_in "Phone", with: "0468363090"
context "with basic details filled" do
before do
toggle_shipping
choose sm1.name
toggle_payment
choose pm1.name
toggle_details
within "#details" do
fill_in "First Name", with: "Will"
fill_in "Last Name", with: "Marshall"
fill_in "Email", with: "test@test.com"
fill_in "Phone", with: "0468363090"
end
toggle_billing
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"
end
toggle_shipping
check "Shipping address same as billing address?"
end
toggle_billing
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"
it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do
place_order
page.should have_content "Your order has been processed successfully"
end
context "with a credit card payment method" do
let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") }
it "takes us to the order confirmation page when submitted with a valid credit card" do
toggle_payment
fill_in 'Card Number', with: "4111111111111111"
select 'February', from: 'secrets.card_month'
select (Date.today.year+1).to_s, from: 'secrets.card_year'
fill_in 'CVV', with: '123'
place_order
page.should have_content "Your order has been processed successfully"
end
end
toggle_shipping
check "Shipping address same as billing address?"
place_order
page.should have_content "Your order has been processed successfully", wait: 10
end
end
end

View File

@@ -30,11 +30,3 @@ describe "AccordionCtrl", ->
scope.order =
id: 129
ctrl = $controller 'AccordionCtrl', {$scope: scope}
it "automatically closes all sections if the entire form is valid", ->
waitsFor ->
(scope.accordion.details and
scope.accordion.shipping and
scope.accordion.payment and
scope.accordion.billing) == false
, "the accordion to close", 100

View File

@@ -10,13 +10,18 @@ describe "CheckoutCtrl", ->
Order =
submit: ->
navigate: ->
bindFieldsToLocalStorage: ->
order:
id: 1
email: "public"
secrets:
card_number: "this is a secret"
describe "with user", ->
beforeEach ->
inject ($controller, $rootScope) ->
scope = $rootScope.$new()
spyOn(Order, "bindFieldsToLocalStorage")
ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: Order, CurrentUser: {}}
it "delegates to the service on submit", ->
@@ -29,6 +34,9 @@ describe "CheckoutCtrl", ->
it "is enabled", ->
expect(scope.enabled).toEqual true
it "triggers localStorage binding", ->
expect(Order.bindFieldsToLocalStorage).toHaveBeenCalled()
describe "without user", ->
beforeEach ->
inject ($controller, $rootScope) ->
@@ -37,3 +45,8 @@ describe "CheckoutCtrl", ->
it "is disabled", ->
expect(scope.enabled).toEqual false
it "does not store secrets in local storage", ->
keys = (localStorage.key(i) for i in [0..localStorage.length])
for key in keys
expect(localStorage.getItem(key)).not.toMatch Order.secrets.card_number

View File

@@ -2,16 +2,22 @@ describe 'Order service', ->
Order = null
orderData = null
$httpBackend = null
CheckoutFormState = null
Navigation = null
flash = null
storage = null
scope = null
beforeEach ->
orderData = {
orderData =
id: 3102
payment_method_id: null
bill_address: {test: "foo"}
email: "test@test.com"
bill_address:
test: "foo"
firstname: "Robert"
lastname: "Harrington"
ship_address: {test: "bar"}
user_id: 901
shipping_methods:
7:
require_ship_address: true
@@ -23,22 +29,51 @@ describe 'Order service', ->
payment_methods:
99:
test: "foo"
}
method_type: "gateway"
123:
test: "bar"
method_type: "check"
angular.module('Darkswarm').value('order', orderData)
module 'Darkswarm'
inject ($injector, _$httpBackend_)->
inject ($injector, _$httpBackend_, _storage_, $rootScope)->
$httpBackend = _$httpBackend_
storage = _storage_
Order = $injector.get("Order")
scope = $rootScope.$new()
scope.Order = Order
Navigation = $injector.get("Navigation")
flash = $injector.get("flash")
CheckoutFormState = $injector.get("CheckoutFormState")
spyOn(Navigation, "go") # Stubbing out writes to window.location
it "defaults to no shipping method", ->
expect(Order.order.shipping_method_id).toEqual null
expect(Order.shippingMethod()).toEqual undefined
it "has a shipping price of zero with no shipping method", ->
expect(Order.shippingPrice()).toEqual 0.0
it "binds to localStorage when given a scope", ->
spyOn(storage, "bind")
Order.fieldsToBind = ["testy"]
Order.bindFieldsToLocalStorage({})
prefix = "order_#{Order.order.id}#{Order.order.user_id}"
expect(storage.bind).toHaveBeenCalledWith({}, "Order.order.testy", {storeName: "#{prefix}_testy"})
expect(storage.bind).toHaveBeenCalledWith({}, "Order.ship_address_same_as_billing", {storeName: "#{prefix}_sameasbilling", defaultValue: true})
it "binds order to local storage", ->
Order.bindFieldsToLocalStorage(scope)
prefix = "order_#{Order.order.id}#{Order.order.user_id}"
expect(localStorage.getItem("#{prefix}_email")).toMatch "test@test.com"
it "does not store secrets in local storage", ->
Order.secrets =
card_number: "superfuckingsecret"
Order.bindFieldsToLocalStorage(scope)
keys = (localStorage.key(i) for i in [0..localStorage.length])
for key in keys
expect(localStorage.getItem(key)).not.toMatch Order.secrets.card_number
describe "with shipping method", ->
beforeEach ->
@@ -57,7 +92,7 @@ describe 'Order service', ->
it 'Gets the current payment method', ->
expect(Order.paymentMethod()).toEqual null
Order.order.payment_method_id = 99
expect(Order.paymentMethod()).toEqual {test: "foo"}
expect(Order.paymentMethod()).toEqual {test: "foo", method_type: "gateway"}
it "Posts the Order to the server", ->
$httpBackend.expectPUT("/checkout", {order: Order.preprocess()}).respond 200, {path: "test"}
@@ -76,14 +111,39 @@ describe 'Order service', ->
$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)
expect(Order.preprocess().ship_address_attributes).not.toBe(undefined)
expect(Order.preprocess().ship_address).toBe(undefined)
describe "data preprocessing", ->
beforeEach ->
Order.order.payment_method_id = 99
it "Munges the order attributes to clone ship address from bill address", ->
CheckoutFormState.ship_address_same_as_billing = false
expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address)
CheckoutFormState.ship_address_same_as_billing = true
expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address)
Order.secrets =
card_number: "1234567890123456"
card_month: "10"
card_year: "2015"
card_verification_value: "123"
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)
it "munges the order attributes to clone ship address from bill address", ->
Order.ship_address_same_as_billing = false
expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address)
Order.ship_address_same_as_billing = true
expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address)
it "creates attributes for card fields", ->
source_attributes = Order.preprocess().payments_attributes[0].source_attributes
expect(source_attributes).toBeDefined()
expect(source_attributes.number).toBe Order.secrets.card_number
expect(source_attributes.month).toBe Order.secrets.card_month
expect(source_attributes.year).toBe Order.secrets.card_year
expect(source_attributes.verification_value).toBe Order.secrets.card_verification_value
expect(source_attributes.first_name).toBe Order.order.bill_address.firstname
expect(source_attributes.last_name).toBe Order.order.bill_address.lastname
it "does not create attributes for card fields when no card is supplied", ->
Order.order.payment_method_id = 123
source_attributes = Order.preprocess().payments_attributes[0].source_attributes
expect(source_attributes).not.toBeDefined()

View File

@@ -445,4 +445,18 @@ describe Enterprise do
supplier.supplied_taxons.should == [taxon1, taxon2]
end
end
describe "presentation of attributes" do
let(:distributor) {
create(:distributor_enterprise,
website: "http://www.google.com",
facebook: "www.facebook.com/roger",
linkedin: "http://linkedin.com")
}
it "strips http and www from url fields" do
distributor.website.should == "google.com"
distributor.facebook.should == "facebook.com/roger"
distributor.linkedin.should == "linkedin.com"
end
end
end