Merge master into pretty-emails

This commit is contained in:
Rob Harrington
2014-11-21 14:43:01 +11:00
56 changed files with 953 additions and 442 deletions

4
.gitignore vendored
View File

@@ -15,6 +15,7 @@ tmp/
.#*
*~
*.~lock.*
tags
.emacs.desktop
.DS_Store
*.sublime-project*
@@ -36,5 +37,4 @@ config/initializers/feature_toggle.rb
NERD_tree*
coverage
libpeerconnection.log
tags
app/assets/javascripts/tags
/config/application.yml

View File

@@ -7,6 +7,7 @@ before_install:
before_script:
- cp config/database.travis.yml config/database.yml
- psql -c 'create database open_food_network_test;' -U postgres
- cp config/application.yml.example config/application.yml
script:
- RAILS_ENV=test bundle exec rake db:migrate --trace
- bundle exec rake spec

View File

@@ -44,6 +44,7 @@ gem 'rack-ssl', :require => 'rack/ssl'
gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg'
gem 'angularjs-file-upload-rails', '~> 1.1.0'
gem 'roadie-rails', '~> 1.0.3'
gem 'figaro'
gem 'foreigner'
gem 'immigrant'

View File

@@ -246,6 +246,9 @@ GEM
railties (>= 3.0.0)
ffaker (1.15.0)
ffi (1.9.3)
figaro (0.7.0)
bundler (~> 1.0)
rails (>= 3, < 5)
fog (1.14.0)
builder
excon (~> 0.25.0)
@@ -537,6 +540,7 @@ DEPENDENCIES
debugger-linecache
deface!
factory_girl_rails
figaro
foreigner
foundation-icons-sass-rails
foundation-rails

View File

@@ -48,6 +48,11 @@ Install the project's gem dependencies:
bundle install
Configure the site:
cp config/application.yml.example config/application.yml
edit config/application.yml
Create the development and test databases, using the settings specified in `config/database.yml`:
rake db:setup
@@ -56,7 +61,7 @@ Then load the schema and some seed data with the following command:
rake db:schema:load db:seed
Load some default data for your environment
Load some default data for your environment:
rake openfoodnetwork:dev:load_sample_data
@@ -69,7 +74,7 @@ At long last, your dreams of spinning up a development server can be realised:
Tests, both unit and integration, are based on RSpec. To run the test suite, first prepare the test database:
bundle exec rake db:test:load
bundle exec rake db:test:prepare
Then the tests can be run with:
@@ -90,7 +95,7 @@ usage instructions.
* David Cook (http://github.com/dacook)
* Will Marshall (http://soundcloud.com/willmarshall)
* Laura Summers (https://github.com/summerscope)
* Maikel Linke (https://github.com/mkllnk)
## Licence

View File

@@ -1,6 +1,4 @@
angular.module("ofn.admin").controller "AdminProductEditCtrl", [
"$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "Taxons", "SpreeApiKey",
($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiKey) ->
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons, SpreeApiAuth) ->
$scope.loading = true
$scope.updateStatusMessage =
@@ -38,60 +36,35 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
$scope.producerFilter = "0"
$scope.categoryFilter = "0"
$scope.products = []
$scope.products = BulkProducts.products
$scope.filteredProducts = []
$scope.currentFilters = []
$scope.limit = 15
$scope.productsWithUnsavedVariants = []
$scope.query = ""
$scope.DisplayProperties = DisplayProperties
$scope.initialise = ->
authorise_api_reponse = ""
dataFetcher("/api/users/authorise_api?token=" + SpreeApiKey).then (data) ->
authorise_api_reponse = data
$scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised"
if $scope.spree_api_key_ok
$http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey
$scope.fetchProducts()
else if authorise_api_reponse.hasOwnProperty("error")
$scope.api_error_msg = authorise_api_reponse("error")
else
api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access."
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.$watchCollection '[query, producerFilter, categoryFilter]', ->
$scope.limit = 15 # Reset limit whenever searching
$scope.fetchProducts = -> # WARNING: returns a promise
$scope.fetchProducts = ->
$scope.loading = true
queryString = $scope.currentFilters.reduce (qs,f) ->
return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};"
, ""
return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) ->
$scope.resetProducts data.products
BulkProducts.fetch($scope.currentFilters).then ->
$scope.resetProducts()
$scope.loading = false
if data.pages > 1
for page in [2..data.pages]
dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) ->
for product in data.products
$scope.unpackProduct product
$scope.products.push product
$scope.resetProducts = (data) ->
$scope.products = data
$scope.resetProducts = ->
DirtyProducts.clear()
$scope.setMessage $scope.updateStatusMessage, "", {}, false
$scope.displayProperties ||= {}
angular.forEach $scope.products, (product) ->
$scope.unpackProduct product
$scope.unpackProduct = (product) ->
$scope.displayProperties ||= {}
$scope.displayProperties[product.id] ||= showVariants: false
#$scope.matchProducer product
$scope.loadVariantUnit product
# $scope.matchProducer = (product) ->
# for producer in $scope.producers
@@ -99,37 +72,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
# product.producer = producer
# break
$scope.loadVariantUnit = (product) ->
product.variant_unit_with_scale =
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
"#{product.variant_unit}_#{product.variant_unit_scale}"
else if product.variant_unit
product.variant_unit
else
null
$scope.loadVariantUnitValues product if product.variants
$scope.loadVariantUnitValue product, product.master if product.master
$scope.loadVariantUnitValues = (product) ->
for variant in product.variants
$scope.loadVariantUnitValue product, variant
$scope.loadVariantUnitValue = (product, variant) ->
unit_value = $scope.variantUnitValue product, variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
$scope.variantUnitValue = (product, variant) ->
if variant.unit_value?
if product.variant_unit_scale
variant.unit_value / product.variant_unit_scale
else
variant.unit_value
else
null
$scope.updateOnHand = (product) ->
on_demand_variants = []
@@ -175,7 +117,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
on_hand: null
price: null
$scope.productsWithUnsavedVariants.push product
$scope.displayProperties[product.id].showVariants = true
DisplayProperties.setShowVariants product.id, true
$scope.nextVariantId = ->
@@ -183,12 +125,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
$scope.variantIdCounter -= 1
$scope.variantIdCounter
$scope.updateVariantLists = (server_products) ->
for product in $scope.productsWithUnsavedVariants
server_product = $scope.findProduct(product.id, server_products)
product.variants = server_product.variants
$scope.loadVariantUnitValues product
$scope.deleteProduct = (product) ->
if confirm("Are you sure?")
$http(
@@ -218,18 +154,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
$scope.cloneProduct = (product) ->
dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) ->
# Ideally we would use Spree's built in respond_override helper here to redirect the
# user after a successful clone with .json in the accept headers
# However, at the time of writing there appears to be an issue which causes the
# respond_with block in the destroy action of Spree::Admin::Product to break
# when a respond_overrride for the clone action is used.
id = data.product.id
dataFetcher("/api/products/" + id + "?template=bulk_show").then (data) ->
newProduct = data
$scope.unpackProduct newProduct
$scope.products.push newProduct
BulkProducts.cloneProduct product
$scope.hasVariants = (product) ->
product.variants.length > 0
@@ -270,7 +195,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
filters: $scope.currentFilters
).success((data) ->
DirtyProducts.clear()
$scope.updateVariantLists(data.products)
BulkProducts.updateVariantLists(data.products, $scope.productsWithUnsavedVariants)
$timeout -> $scope.displaySuccess()
).error (data, status) ->
if status == 400 && data.errors? && data.errors.length > 0
@@ -304,16 +229,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
if variant.hasOwnProperty("unit_value_with_description")
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
if match
product = $scope.findProduct(product.id, $scope.products)
product = BulkProducts.find product.id
variant.unit_value = parseFloat(match[1])
variant.unit_value = null if isNaN(variant.unit_value)
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
variant.unit_description = match[3]
$scope.findProduct = (id, product_list) ->
products = (product for product in product_list when product.id == id)
if products.length == 0 then null else products[0]
$scope.incrementLimit = ->
if $scope.limit < $scope.products.length
$scope.limit = $scope.limit + 5
@@ -354,7 +275,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [
, false
else
$scope.setMessage $scope.updateStatusMessage, "", {}, false
]
filterSubmitProducts = (productsToFilter) ->
filteredProducts = []
@@ -386,8 +307,8 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("name")
filteredProduct.name = product.name
hasUpdatableProperty = true
if product.hasOwnProperty("producer")
filteredProduct.supplier_id = product.producer
if product.hasOwnProperty("producer_id")
filteredProduct.supplier_id = product.producer_id
hasUpdatableProperty = true
if product.hasOwnProperty("price")
filteredProduct.price = product.price
@@ -402,8 +323,8 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
if product.hasOwnProperty("category")
filteredProduct.primary_taxon_id = product.category
if product.hasOwnProperty("category_id")
filteredProduct.primary_taxon_id = product.category_id
hasUpdatableProperty = true
if product.hasOwnProperty("available_on")
filteredProduct.available_on = product.available_on

View File

@@ -0,0 +1,27 @@
angular.module("ofn.admin").controller "AdminOverrideVariantsCtrl", ($scope, Indexer, SpreeApiAuth, PagedFetcher, hubs, producers) ->
$scope.hubs = hubs
$scope.hub = null
$scope.products = []
$scope.producers = Indexer.index producers
$scope.initialise = ->
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.fetchProducts = ->
url = "/api/products/distributable?page=::page::;per_page=100"
PagedFetcher.fetch url, (data) => $scope.addProducts data.products
$scope.addProducts = (products) ->
$scope.products = $scope.products.concat products
$scope.selectHub = ->
$scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0]

View File

@@ -1,18 +1,19 @@
angular.module("ofn.admin").directive "ofnToggleVariants", ->
angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) ->
link: (scope, element, attrs) ->
if scope.displayProperties[scope.product.id].showVariants
if DisplayProperties.showVariants scope.product.id
element.removeClass "icon-chevron-right"
element.addClass "icon-chevron-down"
else
element.removeClass "icon-chevron-down"
element.addClass "icon-chevron-right"
element.on "click", ->
scope.$apply ->
if scope.displayProperties[scope.product.id].showVariants
scope.displayProperties[scope.product.id].showVariants = false
if DisplayProperties.showVariants scope.product.id
DisplayProperties.setShowVariants scope.product.id, false
element.removeClass "icon-chevron-down"
element.addClass "icon-chevron-right"
else
scope.displayProperties[scope.product.id].showVariants = true
DisplayProperties.setShowVariants scope.product.id, true
element.removeClass "icon-chevron-right"
element.addClass "icon-chevron-down"

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").filter "category", ($filter) ->
return (products, taxonID) ->
return products if taxonID == "0"
return $filter('filter')( products, { category: taxonID }, true )
return $filter('filter')( products, { category_id: taxonID }, true )

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").filter "producer", ($filter) ->
return (products, producerID) ->
return products if producerID == "0"
$filter('filter')( products, { producer: producerID }, true )
$filter('filter')( products, { producer_id: producerID }, true )

View File

@@ -0,0 +1,74 @@
angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) ->
new class BulkProducts
products: []
fetch: (filters, onComplete) ->
queryString = filters.reduce (qs,f) ->
return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};"
, ""
url = "/api/products/bulk_products?page=::page::;per_page=20;#{queryString}"
PagedFetcher.fetch url, (data) => @addProducts data.products
cloneProduct: (product) ->
dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) =>
# Ideally we would use Spree's built in respond_override helper here to redirect the
# user after a successful clone with .json in the accept headers
# However, at the time of writing there appears to be an issue which causes the
# respond_with block in the destroy action of Spree::Admin::Product to break
# when a respond_overrride for the clone action is used.
id = data.product.id
dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) =>
@addProducts [newProduct]
updateVariantLists: (serverProducts, productsWithUnsavedVariants) ->
for product in productsWithUnsavedVariants
server_product = @findProductInList(product.id, serverProducts)
product.variants = server_product.variants
@loadVariantUnitValues product
find: (id) ->
@findProductInList id, @products
findProductInList: (id, product_list) ->
products = (product for product in product_list when product.id == id)
if products.length == 0 then null else products[0]
addProducts: (products) ->
for product in products
@unpackProduct product
@products.push product
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product
loadVariantUnit: (product) ->
product.variant_unit_with_scale =
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
"#{product.variant_unit}_#{product.variant_unit_scale}"
else if product.variant_unit
product.variant_unit
else
null
@loadVariantUnitValues product if product.variants
@loadVariantUnitValue product, product.master if product.master
loadVariantUnitValues: (product) ->
for variant in product.variants
@loadVariantUnitValue product, variant
loadVariantUnitValue: (product, variant) ->
unit_value = @variantUnitValue product, variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
variantUnitValue: (product, variant) ->
if variant.unit_value?
if product.variant_unit_scale
variant.unit_value / product.variant_unit_scale
else
variant.unit_value
else
null

View File

@@ -0,0 +1,14 @@
angular.module("ofn.admin").factory "DisplayProperties", ->
new class DisplayProperties
displayProperties: {}
showVariants: (product_id) ->
@initProduct product_id
@displayProperties[product_id].showVariants
setShowVariants: (product_id, showVariants) ->
@initProduct product_id
@displayProperties[product_id].showVariants = showVariants
initProduct: (product_id) ->
@displayProperties[product_id] ||= {showVariants: false}

View File

@@ -0,0 +1,13 @@
# Convert an array of objects into a hash, indexed by the objects' ids
#
# producers = [{id: 1, name: 'one'}, {id: 2, name: 'two'}]
# Indexer.index producers
# -> {1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}}
angular.module("ofn.admin").factory 'Indexer', ->
new class Indexer
index: (data) ->
index = []
for e in data
index[e.id] = e
index

View File

@@ -0,0 +1,16 @@
angular.module("ofn.admin").factory "PagedFetcher", (dataFetcher) ->
new class PagedFetcher
# Given a URL like http://example.com/foo?page=::page::&per_page=20
# And the response includes an attribute pages with the number of pages to fetch
# Fetch each page async, and call the processData callback with the resulting data
fetch: (url, processData) ->
dataFetcher(@urlForPage(url, 1)).then (data) =>
processData data
if data.pages > 1
for page in [2..data.pages]
dataFetcher(@urlForPage(url, page)).then (data) ->
processData data
urlForPage: (url, page) ->
url.replace("::page::", page)

View File

@@ -0,0 +1,16 @@
angular.module("ofn.admin").factory "SpreeApiAuth", ($q, $http, SpreeApiKey) ->
new class SpreeApiAuth
authorise: ->
deferred = $q.defer()
$http.get("/api/users/authorise_api?token=" + SpreeApiKey)
.success (response) ->
if response?.success == "Use of API Authorised"
$http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey
deferred.resolve()
.error (response) ->
error = response?.error || "You are unauthorised to access this page."
deferred.reject(error)
deferred.promise

View File

@@ -2,8 +2,9 @@ require 'open_food_network/spree_api_key_loader'
Spree::Admin::ProductsController.class_eval do
include OpenFoodNetwork::SpreeApiKeyLoader
include OrderCyclesHelper
before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update]
before_filter :load_spree_api_key, :only => :bulk_edit
before_filter :load_spree_api_key, :only => [:bulk_edit, :override_variants]
alias_method :location_after_save_original, :location_after_save
@@ -48,6 +49,12 @@ Spree::Admin::ProductsController.class_eval do
end
end
def override_variants
@hubs = order_cycle_hub_enterprises(without_validation: true)
@producers = order_cycle_producer_enterprises
end
protected
def location_after_save
if URI(request.referer).path == '/admin/products/bulk_edit'

View File

@@ -9,6 +9,7 @@ Spree::Api::ProductsController.class_eval do
respond_with(@products, default_template: :index)
end
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
@products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products.
merge(product_scope).
@@ -19,6 +20,20 @@ Spree::Api::ProductsController.class_eval do
render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json
end
def distributable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
order_cycle_enterprises.is_primary_producer.by_name
@products = Spree::Product.scoped.
merge(product_scope).
where(supplier_id: producers).
by_producer.by_name.
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json
end
def soft_delete
authorize! :delete, Spree::Product
@product = find_product(params[:product_id])

View File

@@ -25,10 +25,18 @@ module Admin
admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer
end
def admin_inject_hubs
admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer
end
def admin_inject_producers
admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer
end
def admin_inject_products
admin_inject_json_ams_array "ofn.admin", "products", @products, Spree::Api::ProductSerializer
end
def admin_inject_taxons
admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer
end
@@ -49,12 +57,12 @@ module Admin
def admin_inject_json_ams(ngModule, name, data, serializer, opts = {})
json = serializer.new(data).to_json
json = serializer.new(data, scope: spree_current_user).to_json
render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json}
end
def admin_inject_json_ams_array(ngModule, name, data, serializer, opts = {})
json = ActiveModel::ArraySerializer.new(data, {each_serializer: serializer}.merge(opts)).to_json
json = ActiveModel::ArraySerializer.new(data, {each_serializer: serializer, scope: spree_current_user}.merge(opts)).to_json
render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json}
end
end

View File

@@ -15,23 +15,27 @@ module OrderCyclesHelper
order_cycle_permitted_enterprises.is_distributor.by_name
end
def order_cycle_hub_enterprises
def order_cycle_hub_enterprises(options={})
enterprises = order_cycle_permitted_enterprises.is_distributor.by_name
enterprises.map do |e|
disabled_message = nil
if e.shipping_methods.empty? && e.payment_methods.available.empty?
disabled_message = 'no shipping or payment methods'
elsif e.shipping_methods.empty?
disabled_message = 'no shipping methods'
elsif e.payment_methods.available.empty?
disabled_message = 'no payment methods'
end
if options[:without_validation]
enterprises
else
enterprises.map do |e|
disabled_message = nil
if e.shipping_methods.empty? && e.payment_methods.available.empty?
disabled_message = 'no shipping or payment methods'
elsif e.shipping_methods.empty?
disabled_message = 'no shipping methods'
elsif e.payment_methods.available.empty?
disabled_message = 'no payment methods'
end
if disabled_message
["#{e.name} (#{disabled_message})", e.id, {disabled: true}]
else
[e.name, e.id]
if disabled_message
["#{e.name} (#{disabled_message})", e.id, {disabled: true}]
else
[e.name, e.id]
end
end
end
end

View File

@@ -97,8 +97,20 @@ Spree::Order.class_eval do
def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil)
current_item = find_line_item_by_variant(variant)
if current_item
current_item.quantity += quantity
current_item.max_quantity += max_quantity.to_i
Bugsnag.notify(RuntimeError.new("Order populator weirdness"), {
current_item: current_item.as_json.pretty_inspect,
line_items: line_items.map(&:id),
reloaded: line_items(:reload).map(&:id),
variant: variant.as_json.pretty_inspect
})
current_item.quantity = quantity
current_item.max_quantity = max_quantity
# This is the original behaviour, behaviour above is so that we can resolve the order populator bug
# current_item.quantity ||= 0
# current_item.max_quantity ||= 0
# current_item.quantity += quantity.to_i
# current_item.max_quantity += max_quantity.to_i
current_item.currency = currency unless currency.nil?
current_item.save
else

View File

@@ -87,6 +87,9 @@ Spree::Product.class_eval do
merge(Exchange.outgoing).
where('order_cycles.id IS NOT NULL') }
scope :by_producer, joins(:supplier).order('enterprises.name')
scope :by_name, order('name')
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
@@ -185,7 +188,7 @@ Spree::Product.class_eval do
if variant_unit_changed?
option_types.delete self.class.all_variant_unit_option_types
option_types << variant_unit_option_type if variant_unit.present?
variants_including_master.each { |v| v.update_units }
variants_including_master.each &:update_units
end
end

View File

@@ -0,0 +1,8 @@
class VariantOverride < ActiveRecord::Base
belongs_to :variant, class_name: 'Spree::Variant'
belongs_to :hub, class_name: 'Enterprise'
def self.price_for(variant, hub)
VariantOverride.where(variant_id: variant, hub_id: hub).first.andand.price
end
end

View File

@@ -0,0 +1,4 @@
/ insert_bottom "[data-hook='admin_product_sub_tabs']"
-# Commented out until this feature is ready
-#= tab :override_details, url: override_variants_admin_products_path, match_path: '/products/override_variants'

View File

@@ -2,25 +2,25 @@ class Spree::Api::ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand
attributes :on_hand, :price, :available_on, :permalink_live
has_one :supplier, key: :producer, embed: :id
has_one :primary_taxon, key: :category, embed: :id
has_one :supplier, key: :producer_id, embed: :id
has_one :primary_taxon, key: :category_id, embed: :id
has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids
has_one :master, serializer: Spree::Api::VariantSerializer
def on_hand
object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand"
end
def price
object.price.nil? ? '0.0' : object.price
end
def available_on
object.available_on.blank? ? "" : object.available_on.strftime("%F %T")
end
def permalink_live
object.permalink
end
end
end

View File

@@ -26,7 +26,7 @@
.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"}
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"}
%a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"}
%i{ng: {class: "hub.icon_font"}}
%span.margin-top.hub-name-listing {{ hub.name | truncate:40}}
@@ -36,5 +36,5 @@
%span.margin-top {{ hub.address.state_name | uppercase }}
.columns.small-6.medium-3.large-4.text-right
%span.margin-top{ bo: { if: "!current()" } }
%span.margin-top{ bo: { if: "!current()" } }
%em Profile only

View File

@@ -3,7 +3,7 @@
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' }
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" }
%td.producer{ 'ng-show' => 'columns.producer.visible' }
%select.select2.fullwidth{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' }
%select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
%td.name{ 'ng-show' => 'columns.name.visible' }
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
%td.unit{ 'ng-show' => 'columns.unit.visible' }
@@ -19,7 +19,7 @@
%span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' }
%input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' }
%td.category{ 'ng-if' => 'columns.category.visible' }
%input.fullwidth{ :type => 'text', id: "p{{product.id}}_category", 'ng-model' => 'product.category', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category' }
%input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id' }
%td.available_on{ 'ng-show' => 'columns.available_on.visible' }
%input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" }
%td.actions

View File

@@ -1,4 +1,4 @@
%tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
%tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'DisplayProperties.showVariants(product.id)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
%td.left-actions
%a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" }
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" }

View File

@@ -0,0 +1,9 @@
= render 'spree/admin/products/override_variants/header'
= render 'spree/admin/products/override_variants/data'
%div{ ng: { app: 'ofn.admin', controller: 'AdminOverrideVariantsCtrl', init: 'initialise()' } }
= render 'spree/admin/products/override_variants/hub_choice'
%h2{ng: {show: 'hub'}} {{ hub.name }}
= render 'spree/admin/products/override_variants/products'

View File

@@ -0,0 +1,3 @@
= admin_inject_spree_api_key
= admin_inject_hubs
= admin_inject_producers

View File

@@ -0,0 +1,4 @@
- content_for :page_title do
Override Product Details
= render :partial => 'spree/admin/shared/product_sub_menu'

View File

@@ -0,0 +1,7 @@
.row
.two.columns.alpha
Hub
.four.columns
%select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', 'ng-options' => 'hub.id as hub.name for hub in hubs' }
.ten.columns.omega
%input{ type: 'button', value: 'Go', 'ng-click' => 'selectHub()' }

View File

@@ -0,0 +1,13 @@
%table.index.bulk{ng: {show: 'hub'}}
%thead
%tr
%th Producer
%th Product
%th Price
%th On hand
%tbody
%tr{ng: {repeat: 'product in products'}}
%td {{ producers[product.producer_id].name }}
%td {{ product.name }}
%td {{ product.price }}
%td {{ product.on_hand }}

View File

@@ -1,6 +1,6 @@
Hello,
Welcome to Australia's Open Food Network! Your login email is <%= @user.email %>
Welcome to #{ENV['DEFAULT_COUNTRY']}'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 http://openfoodnetwork.org.au
@@ -10,4 +10,3 @@ Thanks for getting on board and we look forward to introducing you to many more
Cheers,
Kirsten Larsen and the OFN Team

View File

@@ -57,11 +57,11 @@ module Openfoodnetwork
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
config.time_zone = 'Melbourne'
config.time_zone = ENV["TIMEZONE"]
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = 'en'
config.i18n.default_locale = ENV["LOCALE"]
# Setting this to true causes a performance regression in Rails 3.2.17
# When we're on a version with the fix below, we can set it to true

View File

@@ -0,0 +1,13 @@
# Add application configuration variables here, as shown below.
#
# Change this, it has serious security implications.
# Minimum 30 but usually 128 characters. To obtain run 'rake secret', or faster, 'openssl rand -hex 128'
SECRET_TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TIMEZONE: "Melbourne"
# Default country for dropdowns etc.
DEFAULT_COUNTRY: "Australia"
# Locale for translation.
I18N_LOCALE: "en"
# Spree zone.
CHECKOUT_ZONE: "Australia"

View File

@@ -3,5 +3,9 @@
# Your secret key for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
Openfoodnetwork::Application.config.secret_token = '6d784d49173d0ec820f20cfce151717bd12570e9d261460e9d3c295b90c1fd81e3843eb1bec79d9e6d4a7f04d0fd76170ca0c326ffb0f2da5b7a0b50c7442a4c'
# no regular words or you'll be exposed to dictionary attacks.
Openfoodnetwork::Application.config.secret_token = if Rails.env.development? or Rails.env.test?
('x' * 30) # Meets basic minimum of 30 chars.
else
ENV["SECRET_TOKEN"]
end

View File

@@ -11,11 +11,16 @@ require 'spree/product_filters'
Spree.config do |config|
config.shipping_instructions = true
config.checkout_zone = 'Australia'
config.checkout_zone = ENV["CHECKOUT_ZONE"]
config.address_requires_state = true
# 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded.
config.default_country_id = 12
if Rails.env.test? or Rails.env.development?
config.default_country_id = 12
else
country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"])
config.default_country_id = country.id if country.present?
end
# -- spree_paypal_express
# Auto-capture payments. Without this option, payments must be manually captured in the paypal interface.

View File

@@ -118,6 +118,7 @@ Spree::Core::Engine.routes.prepend do
match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post]
match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post]
match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products"
match '/admin/products/override_variants' => 'admin/products#override_variants', :as => "override_variants_admin_products"
match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management"
match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post]
match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post]
@@ -131,8 +132,11 @@ Spree::Core::Engine.routes.prepend do
end
resources :products do
get :managed, on: :collection
get :bulk_products, on: :collection
collection do
get :managed
get :bulk_products
get :distributable
end
delete :soft_delete
resources :variants do

View File

@@ -0,0 +1,15 @@
class CreateVariantOverrides < ActiveRecord::Migration
def change
create_table :variant_overrides do |t|
t.references :variant
t.references :hub
t.decimal :price, precision: 8, scale: 2
t.integer :count_on_hand
end
add_foreign_key :variant_overrides, :spree_variants, column: :variant_id
add_foreign_key :variant_overrides, :enterprises, column: :hub_id
add_index :variant_overrides, [:variant_id, :hub_id]
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20141023050324) do
ActiveRecord::Schema.define(:version => 20141113053004) do
create_table "adjustment_metadata", :force => true do |t|
t.integer "adjustment_id"
@@ -1033,6 +1033,15 @@ ActiveRecord::Schema.define(:version => 20141023050324) do
t.integer "state_id"
end
create_table "variant_overrides", :force => true do |t|
t.integer "variant_id"
t.integer "hub_id"
t.decimal "price", :precision => 8, :scale => 2
t.integer "count_on_hand"
end
add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id"
add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk"
add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id"
@@ -1190,4 +1199,7 @@ ActiveRecord::Schema.define(:version => 20141023050324) do
add_foreign_key "suburbs", "spree_states", name: "suburbs_state_id_fk", column: "state_id"
add_foreign_key "variant_overrides", "enterprises", name: "variant_overrides_hub_id_fk", column: "hub_id"
add_foreign_key "variant_overrides", "spree_variants", name: "variant_overrides_variant_id_fk", column: "variant_id"
end

View File

@@ -0,0 +1,21 @@
module OpenFoodNetwork
# Variants can have several fields overridden on a per-enterprise basis by the
# VariantOverride model. These overrides can be applied to variants by wrapping their
# products in this proxy, which wraps the product's variants in VariantProxy.
class ProductProxy
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
def initialize(product, hub)
@product = product
@hub = hub
end
def variants
@product.variants.map { |v| VariantProxy.new(v, @hub) }
end
def method_missing(name, *args, &block)
@product.send(name, *args, &block)
end
end
end

View File

@@ -0,0 +1,23 @@
module OpenFoodNetwork
# Variants can have several fields overridden on a per-enterprise basis by the
# VariantOverride model. These overrides can be applied to variants by wrapping in an
# instance of the VariantProxy class. This class proxies most methods back to the wrapped
# variant, but checks for overrides when fetching some properties.
class VariantProxy
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
def initialize(variant, hub)
@variant = variant
@hub = hub
end
def price
VariantOverride.price_for(@variant, @hub) || @variant.price
end
def method_missing(name, *args, &block)
@variant.send(name, *args, &block)
end
end
end

View File

@@ -37,8 +37,8 @@ feature %q{
visit '/admin/products/bulk_edit'
expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s2.name
expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s3.name
expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s2.name
expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s3.name
end
it "displays a date input for available_on for each product, formatted to yyyy-mm-dd hh:mm:ss" do
@@ -302,19 +302,19 @@ feature %q{
within "tr#p_#{p.id}" do
expect(page).to have_field "product_name", with: p.name
expect(page).to have_select "producer", selected: s1.name
expect(page).to have_select "producer_id", selected: s1.name
expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T")
expect(page).to have_field "price", with: "10.0"
expect(page).to have_selector "div#s2id_p#{p.id}_category a.select2-choice"
expect(page).to have_selector "div#s2id_p#{p.id}_category_id a.select2-choice"
expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)"
expect(page).to have_field "on_hand", with: "6"
fill_in "product_name", with: "Big Bag Of Potatoes"
select s2.name, :from => 'producer'
select s2.name, :from => 'producer_id'
fill_in "available_on", with: (3.days.ago.beginning_of_day).strftime("%F %T")
fill_in "price", with: "20"
select "Weight (kg)", from: "variant_unit_with_scale"
select2_select t1.name, from: "p#{p.id}_category"
select2_select t1.name, from: "p#{p.id}_category_id"
fill_in "on_hand", with: "18"
fill_in "display_as", with: "Big Bag"
end
@@ -654,13 +654,13 @@ feature %q{
end
expect(page).to have_selector "a.clone-product", :count => 4
expect(page).to have_field "product_name", with: "COPY OF #{p1.name}"
expect(page).to have_select "producer", selected: "#{p1.supplier.name}"
expect(page).to have_select "producer_id", selected: "#{p1.supplier.name}"
visit '/admin/products/bulk_edit'
expect(page).to have_selector "a.clone-product", :count => 4
expect(page).to have_field "product_name", with: "COPY OF #{p1.name}"
expect(page).to have_select "producer", selected: "#{p1.supplier.name}"
expect(page).to have_select "producer_id", selected: "#{p1.supplier.name}"
end
end
end
@@ -764,8 +764,8 @@ feature %q{
it "shows only suppliers that I manage or have permission to" do
visit '/admin/products/bulk_edit'
expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name
expect(page).to have_no_select 'producer', with_options: [supplier_unmanaged.name]
expect(page).to have_select 'producer_id', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name
expect(page).to have_no_select 'producer_id', with_options: [supplier_unmanaged.name]
end
it "shows inactive products that I supply" do
@@ -807,13 +807,13 @@ feature %q{
within "tr#p_#{p.id}" do
expect(page).to have_field "product_name", with: p.name
expect(page).to have_select "producer", selected: supplier_permitted.name
expect(page).to have_select "producer_id", selected: supplier_permitted.name
expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T")
expect(page).to have_field "price", with: "10.0"
expect(page).to have_field "on_hand", with: "6"
fill_in "product_name", with: "Big Bag Of Potatoes"
select(supplier_managed2.name, :from => 'producer')
select supplier_managed2.name, :from => 'producer_id'
fill_in "available_on", with: (3.days.ago.beginning_of_day).strftime("%F %T")
fill_in "price", with: "20"
select "Weight (kg)", from: "variant_unit_with_scale"

View File

@@ -0,0 +1,52 @@
require 'spec_helper'
feature %q{
As an Administrator
With products I can add to order cycles
I want to override the stock level and price of those products
Without affecting other hubs that share the same products
}, js: true do
include AuthenticationWorkflow
include WebHelper
before do
login_to_admin_section
end
use_short_wait
let!(:hub) { create(:distributor_enterprise) }
let!(:producer) { create(:supplier_enterprise) }
describe "selecting a hub" do
it "displays a list of hub choices" do
visit '/admin/products/override_variants'
page.should have_select2 'hub_id', options: ['', hub.name]
end
it "displays the hub" do
visit '/admin/products/override_variants'
select2_select hub.name, from: 'hub_id'
click_button 'Go'
page.should have_selector 'h2', text: hub.name
end
end
context "when a hub is selected" do
let!(:product) { create(:simple_product, supplier: producer, price: 1.23, on_hand: 12) }
before do
visit '/admin/products/override_variants'
select2_select hub.name, from: 'hub_id'
click_button 'Go'
end
it "displays the list of products" do
page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND']
page.should have_table_row [producer.name, product.name, '1.23', '12']
end
it "products values are affected by overrides"
end
end

View File

@@ -0,0 +1,38 @@
describe "OverrideVariantsCtrl", ->
ctrl = null
scope = null
hubs = [{id: 1, name: 'Hub'}]
producers = [{id: 2, name: 'Producer'}]
products = [{id: 1, name: 'Product'}]
beforeEach ->
module 'ofn.admin'
module ($provide) ->
$provide.value 'SpreeApiKey', 'API_KEY'
null
scope = {}
inject ($controller, Indexer) ->
ctrl = $controller 'AdminOverrideVariantsCtrl', {$scope: scope, Indexer: Indexer, hubs: hubs, producers: producers, products: products}
it "initialises the hub list and the chosen hub", ->
expect(scope.hubs).toEqual hubs
expect(scope.hub).toBeNull
it "adds products", ->
expect(scope.products).toEqual []
scope.addProducts ['a', 'b']
expect(scope.products).toEqual ['a', 'b']
scope.addProducts ['c', 'd']
expect(scope.products).toEqual ['a', 'b', 'c', 'd']
describe "selecting a hub", ->
it "sets the chosen hub", ->
scope.hub_id = 1
scope.selectHub()
expect(scope.hub).toEqual hubs[0]
it "does nothing when no selection has been made", ->
scope.hub_id = ''
scope.selectHub
expect(scope.hub).toBeNull

View File

@@ -0,0 +1,204 @@
describe "BulkProducts service", ->
BulkProducts = $httpBackend = null
beforeEach ->
module "ofn.admin"
beforeEach inject (_BulkProducts_, _$httpBackend_) ->
BulkProducts = _BulkProducts_
$httpBackend = _$httpBackend_
describe "fetching products", ->
beforeEach ->
spyOn BulkProducts, 'addProducts'
it "makes a standard call to dataFetcher when no filters exist", ->
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products"
BulkProducts.fetch [], ->
$httpBackend.flush()
it "makes more calls to dataFetcher if more pages exist", ->
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 }
$httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] }
BulkProducts.fetch [], ->
$httpBackend.flush()
it "applies filters when they are supplied", ->
filter =
property:
name: "Name"
db_column: "name"
predicate:
name: "Equals"
predicate: "eq"
value: "Product1"
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products"
BulkProducts.fetch [filter], ->
$httpBackend.flush()
describe "cloning products", ->
it "clones products using a http get request to /admin/products/(permalink)/clone.json", ->
BulkProducts.products = [
id: 13
permalink_live: "oranges"
]
$httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200,
product:
id: 17
name: "new_product"
$httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [
id: 17
name: "new_product"
]
BulkProducts.cloneProduct BulkProducts.products[0]
$httpBackend.flush()
it "adds the product", ->
originalProduct =
id: 16
permalink_live: "oranges"
clonedProduct =
id: 17
spyOn(BulkProducts, "addProducts")
BulkProducts.products = [originalProduct]
$httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200,
product: clonedProduct
$httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, clonedProduct
BulkProducts.cloneProduct BulkProducts.products[0]
$httpBackend.flush()
expect(BulkProducts.addProducts).toHaveBeenCalledWith [clonedProduct]
describe "preparing products", ->
beforeEach ->
spyOn BulkProducts, "loadVariantUnit"
it "calls loadVariantUnit for the product", ->
product = {id: 123}
BulkProducts.unpackProduct product
expect(BulkProducts.loadVariantUnit).toHaveBeenCalled()
describe "loading variant unit", ->
describe "setting product variant_unit_with_scale field", ->
it "sets by combining variant_unit and variant_unit_scale", ->
product =
variant_unit: "volume"
variant_unit_scale: .001
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "volume_0.001"
it "sets to null when variant_unit is null", ->
product = {variant_unit: null, variant_unit_scale: 1000}
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toBeNull()
it "sets to variant_unit when variant_unit_scale is null", ->
product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "sets to variant_unit when variant_unit is 'items'", ->
product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "loads data for variants (incl. master)", ->
spyOn BulkProducts, "loadVariantUnitValues"
spyOn BulkProducts, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
BulkProducts.loadVariantUnit product
expect(BulkProducts.loadVariantUnitValues).toHaveBeenCalledWith product
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.master
it "loads data for variants (excl. master)", ->
spyOn BulkProducts, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
BulkProducts.loadVariantUnitValues product
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0]
expect(BulkProducts.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master
describe "setting variant unit_value_with_description", ->
it "sets by combining unit_value and unit_description", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0]).toEqual
id: 1
unit_value: 1
unit_description: '(bottle)'
unit_value_with_description: '1 (bottle)'
it "uses unit_value when description is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '1'
it "uses unit_description when value is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_description: 'Small'}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
it "converts values from base value to chosen unit", ->
product =
variant_unit_scale: 1000.0
variants: [{id: 1, unit_value: 2500}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
it "displays a unit_value of zero", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 0}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '0'
describe "calculating the scaled unit value for a variant", ->
it "returns the scaled value when variant has a unit_value", ->
product = {variant_unit_scale: 0.001}
variant = {unit_value: 5}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5000
it "returns the unscaled value when the product has no scale", ->
product = {}
variant = {unit_value: 5}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5
it "returns zero when the value is zero", ->
product = {}
variant = {unit_value: 0}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 0
it "returns null when the variant has no unit_value", ->
product = {}
variant = {}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual null
describe "fetching a product by id", ->
it "returns the product when it is present", ->
product = {id: 123}
BulkProducts.products = [product]
expect(BulkProducts.find(123)).toEqual product
it "returns null when the product is not present", ->
BulkProducts.products = []
expect(BulkProducts.find(123)).toBeNull()

View File

@@ -0,0 +1,17 @@
describe "DisplayProperties", ->
DisplayProperties = null
beforeEach ->
module "ofn.admin"
beforeEach inject (_DisplayProperties_) ->
DisplayProperties = _DisplayProperties_
it "defaults showVariants to false", ->
expect(DisplayProperties.showVariants(123)).toEqual false
it "sets the showVariants value", ->
DisplayProperties.setShowVariants(123, true)
expect(DisplayProperties.showVariants(123)).toEqual true
DisplayProperties.setShowVariants(123, false)
expect(DisplayProperties.showVariants(123)).toEqual false

View File

@@ -0,0 +1,13 @@
describe "indexer", ->
Indexer = null
beforeEach ->
module "ofn.admin"
beforeEach inject (_Indexer_) ->
Indexer = _Indexer_
it "indexes an array of objects by id", ->
objects = [{id: 1, name: 'one'}, {id: 2, name: 'two'}]
index = Indexer.index objects
expect(index).toEqual({1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}})

View File

@@ -0,0 +1,13 @@
describe "PagedFetcher service", ->
PagedFetcher = null
beforeEach ->
module "ofn.admin"
beforeEach inject (_PagedFetcher_) ->
PagedFetcher = _PagedFetcher_
describe "substituting a page in the URL", ->
it "replaces ::page:: with the given page number", ->
expect(PagedFetcher.urlForPage("http://example.com/foo?page=::page::&per_page=20", 12)).
toEqual "http://example.com/foo?page=12&per_page=20"

View File

@@ -184,7 +184,7 @@ describe "filtering products for submission to database", ->
created_at: null
updated_at: null
count_on_hand: 0
producer: 5
producer_id: 5
group_buy: null
group_buy_unit_size: null
@@ -231,7 +231,7 @@ describe "filtering products for submission to database", ->
]
describe "AdminProductEditCtrl", ->
$ctrl = $scope = $timeout = $httpBackend = DirtyProducts = null
$ctrl = $scope = $timeout = $httpBackend = BulkProducts = DirtyProducts = DisplayProperties = null
beforeEach ->
module "ofn.admin"
@@ -241,12 +241,14 @@ describe "AdminProductEditCtrl", ->
$provide.value 'SpreeApiKey', 'API_KEY'
null
beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _DirtyProducts_) ->
beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_, _DisplayProperties_) ->
$scope = $rootScope.$new()
$ctrl = _$controller_
$timeout = _$timeout_
$httpBackend = _$httpBackend_
BulkProducts = _BulkProducts_
DirtyProducts = _DirtyProducts_
DisplayProperties = _DisplayProperties_
$ctrl "AdminProductEditCtrl", {$scope: $scope, $timeout: $timeout}
)
@@ -262,42 +264,33 @@ describe "AdminProductEditCtrl", ->
describe "fetching products", ->
it "makes a standard call to dataFetcher when no filters exist", ->
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products"
$scope.fetchProducts()
$q = null
deferred = null
beforeEach inject((_$q_) ->
$q = _$q_
)
beforeEach ->
deferred = $q.defer()
deferred.resolve()
spyOn $scope, "resetProducts"
spyOn(BulkProducts, "fetch").andReturn deferred.promise
it "calls resetProducts after data has been received", ->
spyOn $scope, "resetProducts"
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: "list of products" }
$scope.fetchProducts()
$httpBackend.flush()
expect($scope.resetProducts).toHaveBeenCalledWith "list of products"
it "calls makes more calls to dataFetcher if more pages exist", ->
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 }
$httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] }
$scope.fetchProducts()
$httpBackend.flush()
it "applies filters when they are present", ->
filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"}
$scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing
expect($scope.currentFilters).toEqual [filter]
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products"
$scope.fetchProducts()
$httpBackend.flush()
$scope.$digest()
expect($scope.resetProducts).toHaveBeenCalled()
it "sets the loading property to true before fetching products and unsets it when loading is complete", ->
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products"
$scope.fetchProducts()
expect($scope.loading).toEqual true
$httpBackend.flush()
$scope.$digest()
expect($scope.loading).toEqual false
describe "resetting products", ->
beforeEach ->
spyOn $scope, "unpackProduct"
spyOn DirtyProducts, "clear"
$scope.products = {}
$scope.resetProducts [
@@ -311,153 +304,9 @@ describe "AdminProductEditCtrl", ->
}
]
it "sets products to the value of 'data'", ->
expect($scope.products).toEqual [
{
id: 1
name: "P1"
}
{
id: 3
name: "P2"
}
]
it "resets dirtyProducts", ->
expect(DirtyProducts.clear).toHaveBeenCalled()
it "calls unpackProduct once for each product", ->
expect($scope.unpackProduct.calls.length).toEqual 2
describe "preparing products", ->
beforeEach ->
spyOn $scope, "loadVariantUnit"
it "initialises display properties for the product", ->
product = {id: 123}
$scope.displayProperties = {}
$scope.unpackProduct product
expect($scope.displayProperties[123]).toEqual {showVariants: false}
it "calls loadVariantUnit for the product", ->
product = {id: 123}
$scope.displayProperties = {}
$scope.unpackProduct product
expect($scope.loadVariantUnit.calls.length).toEqual 1
describe "loading variant unit", ->
describe "setting product variant_unit_with_scale field", ->
it "sets by combining variant_unit and variant_unit_scale", ->
product =
variant_unit: "volume"
variant_unit_scale: .001
$scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "volume_0.001"
it "sets to null when variant_unit is null", ->
product = {variant_unit: null, variant_unit_scale: 1000}
$scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toBeNull()
it "sets to variant_unit when variant_unit_scale is null", ->
product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
$scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "sets to variant_unit when variant_unit is 'items'", ->
product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
$scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "loads data for variants (incl. master)", ->
spyOn $scope, "loadVariantUnitValues"
spyOn $scope, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
$scope.loadVariantUnit product
expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product
expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master
it "loads data for variants (excl. master)", ->
spyOn $scope, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
$scope.loadVariantUnitValues product
expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0]
expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master
describe "setting variant unit_value_with_description", ->
it "sets by combining unit_value and unit_description", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
$scope.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0]).toEqual
id: 1
unit_value: 1
unit_description: '(bottle)'
unit_value_with_description: '1 (bottle)'
it "uses unit_value when description is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1}]
$scope.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '1'
it "uses unit_description when value is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_description: 'Small'}]
$scope.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
it "converts values from base value to chosen unit", ->
product =
variant_unit_scale: 1000.0
variants: [{id: 1, unit_value: 2500}]
$scope.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
it "displays a unit_value of zero", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 0}]
$scope.loadVariantUnitValues product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '0'
describe "calculating the scaled unit value for a variant", ->
it "returns the scaled value when variant has a unit_value", ->
product = {variant_unit_scale: 0.001}
variant = {unit_value: 5}
expect($scope.variantUnitValue(product, variant)).toEqual 5000
it "returns the unscaled value when the product has no scale", ->
product = {}
variant = {unit_value: 5}
expect($scope.variantUnitValue(product, variant)).toEqual 5
it "returns zero when the value is zero", ->
product = {}
variant = {unit_value: 0}
expect($scope.variantUnitValue(product, variant)).toEqual 0
it "returns null when the variant has no unit_value", ->
product = {}
variant = {}
expect($scope.variantUnitValue(product, variant)).toEqual null
describe "updating the product on hand count", ->
it "updates when product is not available on demand", ->
@@ -676,7 +525,7 @@ describe "AdminProductEditCtrl", ->
testProduct = {id: 123}
beforeEach ->
$scope.products = [testProduct]
BulkProducts.products = [testProduct]
it "extracts unit_value and unit_description from unit_value_with_description", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
@@ -738,7 +587,7 @@ describe "AdminProductEditCtrl", ->
it "converts value from chosen unit to base unit", ->
testProduct = {id: 123, variant_unit_scale: 1000}
testVariant = {unit_value_with_description: "250.5"}
$scope.products = [testProduct]
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual
unit_value: 250500
@@ -748,7 +597,7 @@ describe "AdminProductEditCtrl", ->
it "does not convert value when using a non-scaled unit", ->
testProduct = {id: 123}
testVariant = {unit_value_with_description: "12"}
$scope.products = [testProduct]
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual
unit_value: 12
@@ -814,7 +663,7 @@ describe "AdminProductEditCtrl", ->
it "runs displaySuccess() when post returns success", ->
spyOn $scope, "displaySuccess"
spyOn $scope, "updateVariantLists"
spyOn BulkProducts, "updateVariantLists"
spyOn DirtyProducts, "clear"
$scope.products = [
{
@@ -841,7 +690,7 @@ describe "AdminProductEditCtrl", ->
$timeout.flush()
expect($scope.displaySuccess).toHaveBeenCalled()
expect(DirtyProducts.clear).toHaveBeenCalled()
expect($scope.updateVariantLists).toHaveBeenCalled()
expect(BulkProducts.updateVariantLists).toHaveBeenCalled()
it "runs displayFailure() when post returns an error", ->
spyOn $scope, "displayFailure"
@@ -859,20 +708,10 @@ describe "AdminProductEditCtrl", ->
$httpBackend.flush()
expect(window.alert).toHaveBeenCalledWith("Saving failed with the following error(s):\nan error\n")
describe "fetching a product by id", ->
it "returns the product when it is present", ->
product = {id: 123}
$scope.products = [product]
expect($scope.findProduct(123, $scope.products)).toEqual product
it "returns null when the product is not present", ->
$scope.products = []
expect($scope.findProduct(123, $scope.products)).toBeNull()
describe "adding variants", ->
beforeEach ->
$scope.displayProperties ||= {123: {}}
spyOn DisplayProperties, 'setShowVariants'
it "adds first and subsequent variants", ->
product = {id: 123, variants: []}
@@ -888,7 +727,7 @@ describe "AdminProductEditCtrl", ->
it "shows the variant(s)", ->
product = {id: 123, variants: []}
$scope.addVariant(product)
expect($scope.displayProperties[123].showVariants).toBe(true)
expect(DisplayProperties.setShowVariants).toHaveBeenCalledWith 123, true
describe "deleting products", ->
@@ -1023,85 +862,6 @@ describe "AdminProductEditCtrl", ->
describe "cloning products", ->
it "clones products using a http get request to /admin/products/(permalink)/clone.json", ->
$scope.products = [
id: 13
permalink_live: "oranges"
]
$httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200,
product:
id: 17
name: "new_product"
$httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [
id: 17
name: "new_product"
]
$scope.cloneProduct $scope.products[0]
$httpBackend.flush()
it "adds the newly created product to $scope.products and matches producer", ->
spyOn($scope, "unpackProduct").andCallThrough()
$scope.products = [
id: 13
permalink_live: "oranges"
]
$httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200,
product:
id: 17
name: "new_product"
producer: 6
variants: [
id: 3
name: "V1"
]
$httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200,
id: 17
name: "new_product"
producer: 6
variants: [
id: 3
name: "V1"
]
$scope.cloneProduct $scope.products[0]
$httpBackend.flush()
expect($scope.unpackProduct).toHaveBeenCalledWith
id: 17
name: "new_product"
variant_unit_with_scale: null
producer: 6
variants: [
id: 3
name: "V1"
unit_value_with_description: ""
]
expect($scope.products).toEqual [
{
id: 13
permalink_live: "oranges"
}
{
id: 17
name: "new_product"
variant_unit_with_scale: null
producer: 6
variants: [
id: 3
name: "V1"
unit_value_with_description: ""
]
}
]
describe "filtering products", ->
describe "clearing filters", ->
it "resets filter variables", ->

View File

@@ -0,0 +1,27 @@
require 'open_food_network/product_proxy'
require 'open_food_network/variant_proxy'
module OpenFoodNetwork
describe ProductProxy do
let(:hub) { double(:hub) }
let(:p) { double(:product, name: 'name') }
let(:pp) { ProductProxy.new(p, hub) }
describe "delegating calls to proxied product" do
it "delegates name" do
pp.name.should == 'name'
end
end
describe "fetching the variants" do
let(:v1) { double(:variant) }
let(:v2) { double(:variant) }
let(:p) { double(:product, variants: [v1, v2]) }
it "returns variants wrapped in VariantProxy" do
# #class is proxied too, so we test that it worked by #object_id
pp.variants.map(&:object_id).sort.should_not == [v1.object_id, v2.object_id].sort
end
end
end
end

View File

@@ -0,0 +1,27 @@
require 'open_food_network/variant_proxy'
module OpenFoodNetwork
describe VariantProxy do
let(:hub) { double(:hub) }
let(:v) { double(:variant, sku: 'sku123', price: 'global price') }
let(:vp) { VariantProxy.new(v, hub) }
describe "delegating calls to proxied variant" do
it "delegates sku" do
vp.sku.should == 'sku123'
end
end
describe "looking up the price" do
it "returns the override price when there is one" do
VariantOverride.stub(:price_for) { 'override price' }
vp.price.should == 'override price'
end
it "returns the global price otherwise" do
VariantOverride.stub(:price_for) { nil }
vp.price.should == 'global price'
end
end
end
end

View File

@@ -27,12 +27,6 @@ module Spree
product.should_not be_valid
end
it "defaults available_on to now" do
Timecop.freeze
product = Product.new
product.available_on.should == Time.now
end
it "does not save when master is invalid" do
s = create(:supplier_enterprise)
t = create(:taxon)
@@ -41,6 +35,12 @@ module Spree
product.save.should be_false
end
it "defaults available_on to now" do
Timecop.freeze
product = Product.new
product.available_on.should == Time.now
end
context "when the product has variants" do
let(:product) do
product = create(:simple_product)

View File

@@ -0,0 +1,17 @@
require 'spec_helper'
describe VariantOverride do
describe "looking up prices" do
let(:variant) { create(:variant) }
let(:hub) { create(:distributor_enterprise) }
it "returns the numeric price when present" do
VariantOverride.create!(variant: variant, hub: hub, price: 12.34)
VariantOverride.price_for(variant, hub).should == 12.34
end
it "returns nil otherwise" do
VariantOverride.price_for(variant, hub).should be_nil
end
end
end

View File

@@ -0,0 +1,56 @@
RSpec::Matchers.define :have_select2 do |id, options={}|
# TODO: Implement other have_select options
# http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Matchers#has_select%3F-instance_method
# TODO: Instead of passing in id, use a more general locator
match_for_should do |node|
@id, @options, @node = id, options, node
#id = find_label_by_text(locator)
from = "#s2id_#{id}"
results = []
results << node.has_selector?(from)
if results.all?
results << all_options_present(from, options[:with_options]) if options.key? :with_options
results << exact_options_present(from, options[:options]) if options.key? :options
end
results.all?
end
failure_message_for_should do |actual|
message = "expected to find select2 ##{@id}"
message += " with #{@options.inspect}" if @options.any?
message
end
match_for_should_not do |node|
raise "Not yet implemented"
end
def all_options_present(from, options)
with_select2_open(from) do
options.all? do |option|
@node.has_selector? "div.select2-drop-active ul.select2-results li", text: option
end
end
end
def exact_options_present(from, options)
with_select2_open(from) do
@node.all("div.select2-drop-active ul.select2-results li").map(&:text) == options
end
end
def with_select2_open(from)
find(from).click
r = yield
find(from).click
r
end
end

View File

@@ -129,6 +129,7 @@ module WebHelper
targetted_select2(value, options)
end
# Deprecated: Use have_select2 instead (spec/support/matchers/select2_matchers.rb)
def have_select2_option(value, options)
container = options[:dropdown_css] || ".select2-with-searchbox"
page.execute_script %Q{$('#{options[:from]}').select2('open')}