mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-09 23:06:06 +00:00
Merge master into pretty-emails
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
@@ -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 )
|
||||
@@ -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 )
|
||||
@@ -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
|
||||
@@ -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}
|
||||
13
app/assets/javascripts/admin/services/indexer.js.coffee
Normal file
13
app/assets/javascripts/admin/services/indexer.js.coffee
Normal 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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
8
app/models/variant_override.rb
Normal file
8
app/models/variant_override.rb
Normal 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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1,3 @@
|
||||
= admin_inject_spree_api_key
|
||||
= admin_inject_hubs
|
||||
= admin_inject_producers
|
||||
@@ -0,0 +1,4 @@
|
||||
- content_for :page_title do
|
||||
Override Product Details
|
||||
|
||||
= render :partial => 'spree/admin/shared/product_sub_menu'
|
||||
@@ -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()' }
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
config/application.yml.example
Normal file
13
config/application.yml.example
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
15
db/migrate/20141113053004_create_variant_overrides.rb
Normal file
15
db/migrate/20141113053004_create_variant_overrides.rb
Normal 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
|
||||
14
db/schema.rb
14
db/schema.rb
@@ -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
|
||||
|
||||
21
lib/open_food_network/product_proxy.rb
Normal file
21
lib/open_food_network/product_proxy.rb
Normal 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
|
||||
23
lib/open_food_network/variant_proxy.rb
Normal file
23
lib/open_food_network/variant_proxy.rb
Normal 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
|
||||
@@ -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"
|
||||
|
||||
52
spec/features/admin/override_variants_spec.rb
Normal file
52
spec/features/admin/override_variants_spec.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
13
spec/javascripts/unit/admin/services/indexer_spec.js.coffee
Normal file
13
spec/javascripts/unit/admin/services/indexer_spec.js.coffee
Normal 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'}})
|
||||
13
spec/javascripts/unit/admin/services/paged_fetcher.js.coffee
Normal file
13
spec/javascripts/unit/admin/services/paged_fetcher.js.coffee
Normal 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"
|
||||
@@ -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", ->
|
||||
|
||||
27
spec/lib/open_food_network/product_proxy_spec.rb
Normal file
27
spec/lib/open_food_network/product_proxy_spec.rb
Normal 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
|
||||
27
spec/lib/open_food_network/variant_proxy_spec.rb
Normal file
27
spec/lib/open_food_network/variant_proxy_spec.rb
Normal 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
|
||||
@@ -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)
|
||||
|
||||
17
spec/models/variant_override_spec.rb
Normal file
17
spec/models/variant_override_spec.rb
Normal 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
|
||||
56
spec/support/matchers/select2_matchers.rb
Normal file
56
spec/support/matchers/select2_matchers.rb
Normal 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
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user