mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-31 21:37:16 +00:00
Merge branch 'master' into optimise-shopfront
This commit is contained in:
3
.rspec_parallel
Normal file
3
.rspec_parallel
Normal file
@@ -0,0 +1,3 @@
|
||||
--format progress
|
||||
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
|
||||
--tag ~performance
|
||||
2
Gemfile
2
Gemfile
@@ -49,6 +49,7 @@ 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 'acts-as-taggable-on', '~> 3.4'
|
||||
|
||||
gem 'foreigner'
|
||||
gem 'immigrant'
|
||||
@@ -112,4 +113,5 @@ group :development do
|
||||
gem 'guard-rails'
|
||||
gem 'guard-zeus'
|
||||
gem 'guard-rspec'
|
||||
gem 'parallel_tests'
|
||||
end
|
||||
|
||||
@@ -142,6 +142,8 @@ GEM
|
||||
activesupport (3.2.21)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
acts-as-taggable-on (3.5.0)
|
||||
activerecord (>= 3.2, < 5)
|
||||
acts_as_list (0.1.4)
|
||||
addressable (2.3.3)
|
||||
andand (1.3.3)
|
||||
@@ -362,6 +364,9 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
cocaine (~> 0.5.3)
|
||||
mime-types
|
||||
parallel (1.4.1)
|
||||
parallel_tests (1.3.7)
|
||||
parallel
|
||||
paypal-sdk-core (0.2.10)
|
||||
multi_json (~> 1.0)
|
||||
xml-simple
|
||||
@@ -532,6 +537,7 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
active_model_serializers
|
||||
acts-as-taggable-on (~> 3.4)
|
||||
andand
|
||||
angular-rails-templates
|
||||
angularjs-file-upload-rails (~> 1.1.0)
|
||||
@@ -575,6 +581,7 @@ DEPENDENCIES
|
||||
newrelic_rpm
|
||||
oj
|
||||
paperclip
|
||||
parallel_tests
|
||||
pg
|
||||
poltergeist
|
||||
pry-debugger
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) ->
|
||||
angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) ->
|
||||
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content")
|
||||
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
//= require admin/spree_promo
|
||||
//= require admin/spree_paypal_express
|
||||
//= require ../shared/ng-infinite-scroll.min.js
|
||||
//= require ../shared/ng-tags-input.min.js
|
||||
//= require ./admin
|
||||
//= require ./customers/customers
|
||||
//= require ./dropdown/dropdown
|
||||
//= require ./enterprises/enterprises
|
||||
//= require ./enterprise_groups/enterprise_groups
|
||||
//= require ./index_utils/index_utils
|
||||
//= require ./payment_methods/payment_methods
|
||||
//= require ./products/products
|
||||
//= require ./shipping_methods/shipping_methods
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
|
||||
"$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey"
|
||||
($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey) ->
|
||||
"$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns"
|
||||
($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) ->
|
||||
$scope.loading = true
|
||||
|
||||
$scope.initialiseVariables = ->
|
||||
@@ -18,9 +18,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
|
||||
$scope.selectedUnitsProduct = {};
|
||||
$scope.selectedUnitsVariant = {};
|
||||
$scope.sharedResource = false
|
||||
$scope.predicate = ""
|
||||
$scope.reverse = false
|
||||
$scope.columns =
|
||||
$scope.columns = Columns.setColumns
|
||||
order_no: { name: "Order No.", visible: false }
|
||||
full_name: { name: "Name", visible: true }
|
||||
email: { name: "Email", visible: false }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, tax_categories) ->
|
||||
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories) ->
|
||||
$scope.loading = true
|
||||
|
||||
$scope.StatusMessage = StatusMessage
|
||||
|
||||
$scope.columns =
|
||||
$scope.columns = Columns.setColumns
|
||||
producer: {name: "Producer", visible: true}
|
||||
sku: {name: "SKU", visible: false}
|
||||
name: {name: "Name", visible: true}
|
||||
@@ -109,6 +109,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
|
||||
|
||||
|
||||
$scope.toggleShowAllVariants = ->
|
||||
showVariants = !DisplayProperties.showVariants 0
|
||||
$scope.filteredProducts.forEach (product) ->
|
||||
DisplayProperties.setShowVariants product.id, showVariants
|
||||
DisplayProperties.setShowVariants 0, showVariants
|
||||
|
||||
$scope.addVariant = (product) ->
|
||||
product.variants.push
|
||||
id: $scope.nextVariantId()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) ->
|
||||
$scope.shop = null
|
||||
$scope.shops = shops
|
||||
$scope.submitAll = pendingChanges.submitAll
|
||||
|
||||
$scope.columns = Columns.setColumns
|
||||
email: { name: "Email", visible: true }
|
||||
code: { name: "Code", visible: true }
|
||||
tags: { name: "Tags", visible: true }
|
||||
|
||||
$scope.$watch "shop", ->
|
||||
if $scope.shop?
|
||||
Customers.loaded = false
|
||||
$scope.customers = Customers.index(enterprise_id: $scope.shop.id)
|
||||
|
||||
$scope.loaded = ->
|
||||
Customers.loaded
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown'])
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.customers").directive "tagsWithTranslation", ->
|
||||
restrict: "E"
|
||||
template: "<tags-input ng-model='object.tags'>"
|
||||
scope:
|
||||
object: "="
|
||||
link: (scope, element, attrs) ->
|
||||
scope.$watchCollection "object.tags", ->
|
||||
scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",")
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.customers").factory 'CustomerResource', ($resource) ->
|
||||
$resource('/admin/customers.json', {}, {
|
||||
'index':
|
||||
method: 'GET'
|
||||
isArray: true
|
||||
params:
|
||||
enterprise_id: '@enterprise_id'
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
angular.module("admin.customers").factory 'Customers', (CustomerResource) ->
|
||||
new class Customers
|
||||
customers: []
|
||||
customers_by_id: {}
|
||||
loaded: false
|
||||
|
||||
index: (params={}, callback=null) ->
|
||||
CustomerResource.index params, (data) =>
|
||||
for customer in data
|
||||
@customers.push customer
|
||||
@customers_by_id[customer.id] = customer
|
||||
|
||||
@loaded = true
|
||||
(callback || angular.noop)(@customers)
|
||||
|
||||
@customers
|
||||
@@ -1,25 +0,0 @@
|
||||
angular.module("ofn.admin").directive "ofnLineItemUpdAttr", [
|
||||
"switchClass", "pendingChanges"
|
||||
(switchClass, pendingChanges) ->
|
||||
require: "ngModel"
|
||||
link: (scope, element, attrs, ngModel) ->
|
||||
attrName = attrs.ofnLineItemUpdAttr
|
||||
element.dbValue = scope.$eval(attrs.ngModel)
|
||||
scope.$watch ->
|
||||
scope.$eval(attrs.ngModel)
|
||||
, (value) ->
|
||||
#if ngModel.$dirty
|
||||
# i think i can take this out, this directive is still only called
|
||||
# on a change and only an updated value will create a db call.
|
||||
if value == element.dbValue
|
||||
pendingChanges.remove(scope.line_item.id, attrName)
|
||||
switchClass( element, "", ["update-pending", "update-error", "update-success"], false )
|
||||
else
|
||||
changeObj =
|
||||
lineItem: scope.line_item
|
||||
element: element
|
||||
attrName: attrName
|
||||
url: "/api/orders/#{scope.line_item.order.number}/line_items/#{scope.line_item.id}?line_item[#{attrName}]=#{value}"
|
||||
pendingChanges.add(scope.line_item.id, attrName, changeObj)
|
||||
switchClass( element, "update-pending", ["update-error", "update-success"], false )
|
||||
]
|
||||
@@ -1,10 +1,8 @@
|
||||
angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) ->
|
||||
link: (scope, element, attrs) ->
|
||||
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", ->
|
||||
@@ -16,4 +14,4 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -
|
||||
else
|
||||
DisplayProperties.setShowVariants scope.product.id, true
|
||||
element.removeClass "icon-chevron-right"
|
||||
element.addClass "icon-chevron-down"
|
||||
element.addClass "icon-chevron-down"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) ->
|
||||
$scope.expanded = false
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.click (event) ->
|
||||
event.stopPropagation()
|
||||
scope.$emit "offClick"
|
||||
@@ -1,6 +1,4 @@
|
||||
dropDownModule = angular.module("ofn.dropdown", [])
|
||||
|
||||
dropDownModule.directive "ofnDropDown", ($document) ->
|
||||
angular.module("admin.dropdown").directive "ofnDropDown", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
outsideClickListener = (event) ->
|
||||
unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") ||
|
||||
@@ -20,12 +18,3 @@ dropDownModule.directive "ofnDropDown", ($document) ->
|
||||
scope.$apply ->
|
||||
scope.expanded = true
|
||||
element.addClass "expanded"
|
||||
|
||||
dropDownModule.directive "ofnCloseOnClick", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.click (event) ->
|
||||
event.stopPropagation()
|
||||
scope.$emit "offClick"
|
||||
|
||||
dropDownModule.controller "DropDownCtrl", ($scope) ->
|
||||
$scope.expanded = false
|
||||
1
app/assets/javascripts/admin/dropdown/dropdown.js.coffee
Normal file
1
app/assets/javascripts/admin/dropdown/dropdown.js.coffee
Normal file
@@ -0,0 +1 @@
|
||||
angular.module("admin.dropdown", [])
|
||||
@@ -0,0 +1,4 @@
|
||||
angular.module("admin.indexUtils").controller "ColumnsCtrl", ($scope, Columns) ->
|
||||
$scope.columns = Columns.columns
|
||||
$scope.predicate = ""
|
||||
$scope.reverse = false
|
||||
@@ -0,0 +1,36 @@
|
||||
angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendingChanges) ->
|
||||
scope:
|
||||
object: "&objForUpdate"
|
||||
type: "@objForUpdate"
|
||||
attr: "@attrForUpdate"
|
||||
link: (scope, element, attrs) ->
|
||||
scope.savedValue = scope.object()[scope.attr]
|
||||
|
||||
scope.$watch "object().#{scope.attr}", (value) ->
|
||||
if value == scope.savedValue
|
||||
pendingChanges.remove(scope.object().id, scope.attr)
|
||||
scope.clear()
|
||||
else
|
||||
change =
|
||||
object: scope.object()
|
||||
type: scope.type
|
||||
attr: scope.attr
|
||||
value: value
|
||||
scope: scope
|
||||
scope.pending()
|
||||
pendingChanges.add(scope.object().id, scope.attr, change)
|
||||
|
||||
scope.reset = (value) ->
|
||||
scope.savedValue = value
|
||||
|
||||
scope.success = ->
|
||||
switchClass( element, "update-success", ["update-pending", "update-error"], 3000 )
|
||||
|
||||
scope.pending = ->
|
||||
switchClass( element, "update-pending", ["update-error", "update-success"], false )
|
||||
|
||||
scope.error = ->
|
||||
switchClass( element, "update-error", ["update-pending", "update-success"], false )
|
||||
|
||||
scope.clear = ->
|
||||
switchClass( element, "", ["update-pending", "update-error", "update-success"], false )
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("ofn.admin").directive "ofnToggleColumn", ->
|
||||
angular.module("admin.indexUtils").directive "ofnToggleColumn", ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.addClass "selected" if scope.column.visible
|
||||
element.click "click", ->
|
||||
@@ -8,4 +8,4 @@ angular.module("ofn.admin").directive "ofnToggleColumn", ->
|
||||
element.removeClass "selected"
|
||||
else
|
||||
scope.column.visible = true
|
||||
element.addClass "selected"
|
||||
element.addClass "selected"
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.indexUtils", ['ngResource']).config ($httpProvider) ->
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.indexUtils").factory 'Columns', ->
|
||||
new class Columns
|
||||
columns: {}
|
||||
|
||||
setColumns: (columns) ->
|
||||
@columns = {}
|
||||
@columns[name] = column for name, column of columns
|
||||
@columns
|
||||
@@ -0,0 +1,33 @@
|
||||
angular.module("admin.indexUtils").factory "pendingChanges", (resources) ->
|
||||
new class pendingChanges
|
||||
pendingChanges: {}
|
||||
|
||||
add: (id, attr, change) =>
|
||||
@pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}")
|
||||
@pendingChanges["#{id}"]["#{attr}"] = change
|
||||
|
||||
removeAll: =>
|
||||
@pendingChanges = {}
|
||||
|
||||
remove: (id, attr) =>
|
||||
if @pendingChanges.hasOwnProperty("#{id}")
|
||||
delete @pendingChanges["#{id}"]["#{attr}"]
|
||||
delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1
|
||||
|
||||
submitAll: =>
|
||||
all = []
|
||||
for id, objectChanges of @pendingChanges
|
||||
for attrName, change of objectChanges
|
||||
all.push @submit(change)
|
||||
all
|
||||
|
||||
submit: (change) ->
|
||||
resources.update(change).$promise.then (data) =>
|
||||
@remove change.object.id, change.attr
|
||||
change.scope.reset( data["#{change.attr}"] )
|
||||
change.scope.success()
|
||||
, (error) ->
|
||||
change.scope.error()
|
||||
|
||||
changeCount: (objectChanges) ->
|
||||
Object.keys(objectChanges).length
|
||||
@@ -0,0 +1,30 @@
|
||||
angular.module("admin.indexUtils").factory "resources", ($resource) ->
|
||||
LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json',
|
||||
{ order_number: '@order_number', line_item_id: '@line_item_id'},
|
||||
'update': { method: 'PUT' }
|
||||
Customer = $resource '/admin/customers/:customer_id.json',
|
||||
{ customer_id: '@customer_id'},
|
||||
'update': { method: 'PUT' }
|
||||
|
||||
return {
|
||||
update: (change) ->
|
||||
params = {}
|
||||
data = {}
|
||||
resource = null
|
||||
|
||||
switch change.type
|
||||
when "line_item"
|
||||
resource = LineItem
|
||||
params.order_number = change.object.order.number
|
||||
params.line_item_id = change.object.id
|
||||
data.line_item = {}
|
||||
data.line_item[change.attr] = change.value
|
||||
when "customer"
|
||||
resource = Customer
|
||||
params.customer_id = change.object.id
|
||||
data.customer = {}
|
||||
data.customer[change.attr] = change.value
|
||||
else ""
|
||||
|
||||
resource.update(params, data)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
angular.module("admin.indexUtils").factory "switchClass", ($timeout) ->
|
||||
return (element,classToAdd,removeClasses,timeout) ->
|
||||
$timeout.cancel element.timeout if element.timeout
|
||||
element.removeClass className for className in removeClasses
|
||||
element.addClass classToAdd
|
||||
intRegex = /^\d+$/
|
||||
if timeout && intRegex.test(timeout)
|
||||
element.timeout = $timeout(->
|
||||
element.removeClass classToAdd
|
||||
, timeout, true)
|
||||
@@ -1,13 +0,0 @@
|
||||
angular.module("ofn.admin").factory "dataSubmitter", [
|
||||
"$http", "$q", "switchClass"
|
||||
($http, $q, switchClass) ->
|
||||
return (changeObj) ->
|
||||
deferred = $q.defer()
|
||||
$http.put(changeObj.url).success((data) ->
|
||||
switchClass changeObj.element, "update-success", ["update-pending", "update-error"], 3000
|
||||
deferred.resolve data
|
||||
).error ->
|
||||
switchClass changeObj.element, "update-error", ["update-pending", "update-success"], false
|
||||
deferred.reject()
|
||||
deferred.promise
|
||||
]
|
||||
@@ -3,12 +3,10 @@ angular.module("ofn.admin").factory "DisplayProperties", ->
|
||||
displayProperties: {}
|
||||
|
||||
showVariants: (product_id) ->
|
||||
@initProduct product_id
|
||||
@displayProperties[product_id].showVariants
|
||||
@productProperties(product_id).showVariants
|
||||
|
||||
setShowVariants: (product_id, showVariants) ->
|
||||
@initProduct product_id
|
||||
@displayProperties[product_id].showVariants = showVariants
|
||||
@productProperties(product_id).showVariants = showVariants
|
||||
|
||||
initProduct: (product_id) ->
|
||||
productProperties: (product_id) ->
|
||||
@displayProperties[product_id] ||= {showVariants: false}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
angular.module("ofn.admin").factory "pendingChanges",[
|
||||
"dataSubmitter"
|
||||
(dataSubmitter) ->
|
||||
pendingChanges: {}
|
||||
|
||||
add: (id, attrName, changeObj) ->
|
||||
@pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}")
|
||||
@pendingChanges["#{id}"]["#{attrName}"] = changeObj
|
||||
|
||||
removeAll: ->
|
||||
@pendingChanges = {}
|
||||
|
||||
remove: (id, attrName) ->
|
||||
if @pendingChanges.hasOwnProperty("#{id}")
|
||||
delete @pendingChanges["#{id}"]["#{attrName}"]
|
||||
delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1
|
||||
|
||||
submitAll: ->
|
||||
all = []
|
||||
for id,lineItem of @pendingChanges
|
||||
for attrName,changeObj of lineItem
|
||||
all.push @submit(id, attrName, changeObj)
|
||||
all
|
||||
|
||||
submit: (id, attrName, change) ->
|
||||
dataSubmitter(change).then (data) =>
|
||||
@remove id, attrName
|
||||
change.element.dbValue = data["#{attrName}"]
|
||||
|
||||
changeCount: (lineItem) ->
|
||||
Object.keys(lineItem).length
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
angular.module("ofn.admin").factory "switchClass", [
|
||||
"$timeout"
|
||||
($timeout) ->
|
||||
return (element,classToAdd,removeClasses,timeout) ->
|
||||
$timeout.cancel element.timeout if element.timeout
|
||||
element.removeClass className for className in removeClasses
|
||||
element.addClass classToAdd
|
||||
intRegex = /^\d+$/
|
||||
if timeout && intRegex.test(timeout)
|
||||
element.timeout = $timeout(->
|
||||
element.removeClass classToAdd
|
||||
, timeout, true)
|
||||
]
|
||||
1
app/assets/javascripts/shared/ng-tags-input.min.js
vendored
Executable file
1
app/assets/javascripts/shared/ng-tags-input.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@
|
||||
|
||||
*= require shared/jquery-ui-timepicker-addon
|
||||
*= require shared/textAngular.min
|
||||
*= require shared/ng-tags-input.min
|
||||
|
||||
*= require_self
|
||||
*= require_tree .
|
||||
|
||||
@@ -8,16 +8,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
input.update-pending {
|
||||
border: solid 1px orange;
|
||||
input, div {
|
||||
&.update-pending {
|
||||
border: solid 1px orange;
|
||||
}
|
||||
}
|
||||
|
||||
input.update-error {
|
||||
border: solid 1px red;
|
||||
input, div {
|
||||
&.update-error {
|
||||
border: solid 1px red;
|
||||
}
|
||||
}
|
||||
|
||||
input.update-success {
|
||||
border: solid 1px #9fc820;
|
||||
input, div {
|
||||
&.update-success {
|
||||
border: solid 1px #9fc820;
|
||||
}
|
||||
}
|
||||
|
||||
.no-close .ui-dialog-titlebar-close {
|
||||
@@ -42,4 +48,4 @@ div#group_buy_calculation {
|
||||
.row span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
app/assets/stylesheets/shared/ng-tags-input.min.css
vendored
Executable file
1
app/assets/stylesheets/shared/ng-tags-input.min.css
vendored
Executable file
@@ -0,0 +1 @@
|
||||
tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0;padding-left:5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input.invalid-tag{color:red}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}
|
||||
29
app/controllers/admin/customers_controller.rb
Normal file
29
app/controllers/admin/customers_controller.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module Admin
|
||||
class CustomersController < ResourceController
|
||||
before_filter :load_managed_shops, only: :index, if: :html_request?
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: ActiveModel::ArraySerializer.new( @collection,
|
||||
each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user
|
||||
).to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection
|
||||
return Customer.where("1=0") if html_request? || params[:enterprise_id].nil?
|
||||
enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id])
|
||||
Customer.of(enterprise)
|
||||
end
|
||||
|
||||
def load_managed_shops
|
||||
@shops = Enterprise.managed_by(spree_current_user).is_distributor
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -58,4 +58,8 @@ Spree::Admin::BaseController.class_eval do
|
||||
"Until you set these up, customers will not be able to shop at this hub."
|
||||
end
|
||||
end
|
||||
|
||||
def html_request?
|
||||
request.format.html?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ Spree::Admin::OrdersController.class_eval do
|
||||
|
||||
def managed
|
||||
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
@orders = permissions.editable_orders.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
|
||||
@orders = permissions.editable_orders.order(:id).ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
|
||||
render json: @orders, each_serializer: Api::Admin::OrderSerializer
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ module Admin
|
||||
admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer
|
||||
end
|
||||
|
||||
def admin_inject_shops
|
||||
admin_inject_json_ams_array "admin.customers", "shops", @shops, Api::Admin::IdNameSerializer
|
||||
end
|
||||
|
||||
def admin_inject_hubs
|
||||
admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer
|
||||
end
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
class Customer < ActiveRecord::Base
|
||||
acts_as_taggable
|
||||
|
||||
belongs_to :enterprise
|
||||
belongs_to :user, :class_name => Spree.user_class
|
||||
|
||||
validates :code, presence: true, uniqueness: {scope: :enterprise_id}
|
||||
validates :email, presence: true
|
||||
validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true }
|
||||
validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" }
|
||||
validates :enterprise_id, presence: true
|
||||
|
||||
scope :of, ->(enterprise) { where(enterprise_id: enterprise) }
|
||||
|
||||
before_create :associate_user
|
||||
|
||||
private
|
||||
|
||||
def associate_user
|
||||
self.user = user || Spree::User.find_by_email(email)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@ class Enterprise < ActiveRecord::Base
|
||||
has_and_belongs_to_many :payment_methods, join_table: 'distributors_payment_methods', class_name: 'Spree::PaymentMethod', foreign_key: 'distributor_id'
|
||||
has_many :distributor_shipping_methods, foreign_key: :distributor_id
|
||||
has_many :shipping_methods, through: :distributor_shipping_methods
|
||||
has_many :customers
|
||||
|
||||
delegate :latitude, :longitude, :city, :state_name, :to => :address
|
||||
|
||||
@@ -216,12 +217,16 @@ class Enterprise < ActiveRecord::Base
|
||||
", self.id, self.id)
|
||||
end
|
||||
|
||||
def relatives_including_self
|
||||
Enterprise.where(id: relatives.pluck(:id) | [id])
|
||||
end
|
||||
|
||||
def distributors
|
||||
self.relatives.is_distributor
|
||||
self.relatives_including_self.is_distributor
|
||||
end
|
||||
|
||||
def suppliers
|
||||
self.relatives.is_primary_producer
|
||||
self.relatives_including_self.is_primary_producer
|
||||
end
|
||||
|
||||
def website
|
||||
|
||||
@@ -184,6 +184,8 @@ class AbilityDecorator
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
|
||||
|
||||
can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ Spree::Order.class_eval do
|
||||
belongs_to :order_cycle
|
||||
belongs_to :distributor, :class_name => 'Enterprise'
|
||||
belongs_to :cart
|
||||
belongs_to :customer
|
||||
|
||||
validates :customer, presence: true, if: :require_customer?
|
||||
validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? }
|
||||
attr_accessible :order_cycle_id, :distributor_id
|
||||
|
||||
before_validation :shipping_address_from_distributor
|
||||
before_validation :associate_customer, unless: :customer_is_valid?
|
||||
|
||||
checkout_flow do
|
||||
go_to_state :address
|
||||
@@ -261,4 +264,24 @@ Spree::Order.class_eval do
|
||||
def product_distribution_for(line_item)
|
||||
line_item.variant.product.product_distribution_for self.distributor
|
||||
end
|
||||
|
||||
def require_customer?
|
||||
return true unless new_record? or state == 'cart'
|
||||
end
|
||||
|
||||
def customer_is_valid?
|
||||
return true unless require_customer?
|
||||
customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email)
|
||||
end
|
||||
|
||||
def associate_customer
|
||||
email_for_customer = user.andand.email || email
|
||||
existing_customer = Customer.of(distributor).find_by_email(email_for_customer)
|
||||
if existing_customer
|
||||
self.customer = existing_customer
|
||||
else
|
||||
new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user)
|
||||
self.customer = new_customer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
11
app/serializers/api/admin/customer_serializer.rb
Normal file
11
app/serializers/api/admin/customer_serializer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class Api::Admin::CustomerSerializer < ActiveModel::Serializer
|
||||
attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list
|
||||
|
||||
def tag_list
|
||||
object.tag_list.join(",")
|
||||
end
|
||||
|
||||
def tags
|
||||
object.tag_list.map{ |t| { text: t } }
|
||||
end
|
||||
end
|
||||
72
app/views/admin/customers/index.html.haml
Normal file
72
app/views/admin/customers/index.html.haml
Normal file
@@ -0,0 +1,72 @@
|
||||
- content_for :page_title do
|
||||
%h1.page-title Customers
|
||||
|
||||
= admin_inject_shops
|
||||
|
||||
%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } }
|
||||
.row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } }
|
||||
.five.columns.alpha
|
||||
%h3 Please select a Hub:
|
||||
.four.columns
|
||||
%select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' }
|
||||
.seven.columns.omega
|
||||
|
||||
.row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' }
|
||||
.controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" }
|
||||
.five.columns.alpha
|
||||
%input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' }
|
||||
.five.columns
|
||||
-# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true }
|
||||
-# %span{ :class => 'icon-check' } Actions
|
||||
-# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
-# %div.menu{ 'ng-show' => "expanded" }
|
||||
-# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true }
|
||||
-# %span{ :class => 'three columns omega' } {{action.name }}
|
||||
.three.columns
|
||||
.three.columns.omega
|
||||
%div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' }
|
||||
%span{ :class => 'icon-reorder' } Columns
|
||||
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
%div.menu{ 'ng-show' => "expanded" }
|
||||
%div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true }
|
||||
%span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }}
|
||||
%span{ :class => 'two columns omega' } {{column.name }}
|
||||
.row{ 'ng-if' => 'shop && !loaded()' }
|
||||
.sixteen.columns.alpha#loading
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg" }
|
||||
%h1 LOADING CUSTOMERS
|
||||
.row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'}
|
||||
%h1#no_results No customers found.
|
||||
|
||||
|
||||
.row{ ng: { show: "loaded() && filteredCustomers.length > 0" } }
|
||||
%form{ name: "customers" }
|
||||
%table.index#customers
|
||||
%col.email{ width: "20%"}
|
||||
%col.code{ width: "20%"}
|
||||
%col.tags{ width: "50%"}
|
||||
%col.actions{ width: "10%"}
|
||||
%thead
|
||||
%tr{ ng: { controller: "ColumnsCtrl" } }
|
||||
-# %th.bulk
|
||||
-# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" }
|
||||
%th.email{ 'ng-show' => 'columns.email.visible' }
|
||||
%a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email
|
||||
%th.code{ 'ng-show' => 'columns.code.visible' }
|
||||
%a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code
|
||||
%th.tags{ 'ng-show' => 'columns.tags.visible' } Tags
|
||||
%th.actions
|
||||
Ask?
|
||||
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
|
||||
%tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" }
|
||||
-# %td.bulk
|
||||
-# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' }
|
||||
%td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }}
|
||||
%td.code{ 'ng-show' => 'columns.code.visible' }
|
||||
%input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" }
|
||||
%td.tags{ 'ng-show' => 'columns.tags.visible' }
|
||||
.tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"}
|
||||
%tags_with_translation{ object: 'customer' }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" }
|
||||
%input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' }
|
||||
@@ -3,7 +3,7 @@
|
||||
%shop.darkswarm
|
||||
- content_for :order_cycle_form do
|
||||
|
||||
%div{"ng-controller" => "OrderCycleChangeCtrl"}
|
||||
%div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true}
|
||||
%closing{"ng-if" => "OrderCycle.selected()"}
|
||||
Next order closing
|
||||
%strong {{ OrderCycle.orders_close_at() | date_in_words }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%section.left
|
||||
%a.left-off-canvas-toggle.menu-icon
|
||||
%span
|
||||
%section.right
|
||||
%section.right{"ng-cloak" => true}
|
||||
.cart
|
||||
= render partial: "shared/menu/cart"
|
||||
%a{href: main_app.shop_path}
|
||||
@@ -11,34 +11,34 @@
|
||||
%aside.left-off-canvas-menu.show-for-medium-down
|
||||
%ul.off-canvas-list
|
||||
%li.ofn-logo
|
||||
%a{href: root_path}
|
||||
%a{href: root_path}
|
||||
%img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"}
|
||||
|
||||
|
||||
- if current_page? root_path
|
||||
%li.li-menu
|
||||
%a{"ofn-scroll-to" => "hubs"}
|
||||
%span.nav-primary
|
||||
%span.nav-primary
|
||||
%i.ofn-i_040-hub
|
||||
Hubs
|
||||
- else
|
||||
%li.li-menu
|
||||
%a{href: root_path + "#/#hubs"}
|
||||
%span.nav-primary
|
||||
%span.nav-primary
|
||||
%i.ofn-i_040-hub
|
||||
Hubs
|
||||
%li.li-menu
|
||||
%a{href: main_app.map_path}
|
||||
%span.nav-primary
|
||||
%span.nav-primary
|
||||
%i.ofn-i_037-map
|
||||
Map
|
||||
%li.li-menu
|
||||
%a{href: main_app.producers_path}
|
||||
%span.nav-primary
|
||||
%span.nav-primary
|
||||
%i.ofn-i_036-producers
|
||||
Producers
|
||||
%li.li-menu
|
||||
%a{href: main_app.groups_path}
|
||||
%span.nav-primary
|
||||
%span.nav-primary
|
||||
%i.ofn-i_035-groups
|
||||
Groups
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null",
|
||||
%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true,
|
||||
"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"}
|
||||
|
||||
// TODO: Needs an ng-show to slide content down
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#tabs{"ng-controller" => "TabsCtrl"}
|
||||
#tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true}
|
||||
.row
|
||||
%tabset
|
||||
-# Build all tabs.
|
||||
- for name, heading_cols in { about: ["About #{current_distributor.name}", 6],
|
||||
producers: ["Producers",2],
|
||||
- for name, heading_cols in { about: ["About #{current_distributor.name}", 6],
|
||||
producers: ["Producers",2],
|
||||
contact: ["Contact",2],
|
||||
groups: ["Groups",2]}
|
||||
groups: ["Groups",2]}
|
||||
-# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl.
|
||||
- heading, cols = heading_cols
|
||||
%tab.columns{heading: heading,
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
%form{ 'ng-model' => "bulk_order_form" }
|
||||
%table.index#listing_orders.bulk{ :class => "sixteen columns alpha" }
|
||||
%thead
|
||||
%tr
|
||||
%tr{ ng: { controller: "ColumnsCtrl" } }
|
||||
%th.bulk
|
||||
%input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" }
|
||||
%th.order_no{ 'ng-show' => 'columns.order_no.visible' }
|
||||
@@ -132,28 +132,28 @@
|
||||
%th.actions
|
||||
Ask?
|
||||
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
|
||||
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" }
|
||||
%td.bulk
|
||||
%input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' }
|
||||
%td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }}
|
||||
%td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }}
|
||||
%td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }}
|
||||
%td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }}
|
||||
%td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }}
|
||||
%td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }}
|
||||
%td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }}
|
||||
%td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }}
|
||||
%td.variant{ 'ng-show' => 'columns.variant.visible' }
|
||||
%a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }}
|
||||
%td.quantity{ 'ng-show' => 'columns.quantity.visible' }
|
||||
%input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" }
|
||||
%td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }}
|
||||
%td.unit_value{ 'ng-show' => 'columns.unit_value.visible' }
|
||||
%input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "unit_value" }
|
||||
%td.price{ 'ng-show' => 'columns.price.visible' }
|
||||
%input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-model' => "line_item.price", 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "price" }
|
||||
%td.actions
|
||||
%a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" }
|
||||
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" }
|
||||
%td.bulk
|
||||
%input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' }
|
||||
%td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }}
|
||||
%td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }}
|
||||
%td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }}
|
||||
%td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }}
|
||||
%td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }}
|
||||
%td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }}
|
||||
%td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }}
|
||||
%td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }}
|
||||
%td.variant{ 'ng-show' => 'columns.variant.visible' }
|
||||
%a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }}
|
||||
%td.quantity{ 'ng-show' => 'columns.quantity.visible' }
|
||||
%input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" }
|
||||
%td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }}
|
||||
%td.unit_value{ 'ng-show' => 'columns.unit_value.visible' }
|
||||
%input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" }
|
||||
%td.price{ 'ng-show' => 'columns.price.visible' }
|
||||
%input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" }
|
||||
%td.actions
|
||||
%a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" }
|
||||
%input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' }
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
%col.actions
|
||||
|
||||
%thead
|
||||
%tr
|
||||
%tr{ ng: { controller: "ColumnsCtrl" } }
|
||||
%th.left-actions
|
||||
%a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' }
|
||||
Expand All
|
||||
%th.producer{ 'ng-show' => 'columns.producer.visible' } Producer
|
||||
%th.sku{ 'ng-show' => 'columns.sku.visible' } SKU
|
||||
%th.name{ 'ng-show' => 'columns.name.visible' } Name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%tr.product{ :id => "p_{{product.id}}" }
|
||||
%td.left-actions
|
||||
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' }
|
||||
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", '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_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
|
||||
|
||||
@@ -10,7 +10,7 @@ development:
|
||||
test:
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
database: open_food_network_test
|
||||
database: open_food_network_test<%= ENV['TEST_ENV_NUMBER'] %>
|
||||
pool: 5
|
||||
host: localhost
|
||||
username: ofn
|
||||
|
||||
@@ -79,6 +79,8 @@ Openfoodnetwork::Application.routes.draw do
|
||||
resources :variant_overrides do
|
||||
post :bulk_update, on: :collection
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# This migration comes from acts_as_taggable_on_engine (originally 1)
|
||||
class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :tags do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
create_table :taggings do |t|
|
||||
t.references :tag
|
||||
|
||||
# You should make sure that the column created is
|
||||
# long enough to store the required class names.
|
||||
t.references :taggable, polymorphic: true
|
||||
t.references :tagger, polymorphic: true
|
||||
|
||||
# Limit is created to prevent MySQL error on index
|
||||
# length for MyISAM table type: http://bit.ly/vgW2Ql
|
||||
t.string :context, limit: 128
|
||||
|
||||
t.datetime :created_at
|
||||
end
|
||||
|
||||
add_index :taggings, :tag_id
|
||||
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :taggings
|
||||
drop_table :tags
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# This migration comes from acts_as_taggable_on_engine (originally 2)
|
||||
class AddMissingUniqueIndices < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_index :tags, :name, unique: true
|
||||
|
||||
remove_index :taggings, :tag_id
|
||||
remove_index :taggings, [:taggable_id, :taggable_type, :context]
|
||||
add_index :taggings,
|
||||
[:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
|
||||
unique: true, name: 'taggings_idx'
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_index :tags, :name
|
||||
|
||||
remove_index :taggings, name: 'taggings_idx'
|
||||
add_index :taggings, :tag_id
|
||||
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# This migration comes from acts_as_taggable_on_engine (originally 3)
|
||||
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :tags, :taggings_count, :integer, default: 0
|
||||
|
||||
ActsAsTaggableOn::Tag.reset_column_information
|
||||
ActsAsTaggableOn::Tag.find_each do |tag|
|
||||
ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :tags, :taggings_count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# This migration comes from acts_as_taggable_on_engine (originally 4)
|
||||
class AddMissingTaggableIndex < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_index :taggings, [:taggable_id, :taggable_type, :context]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# This migration comes from acts_as_taggable_on_engine (originally 5)
|
||||
# This migration is added to circumvent issue #623 and have special characters
|
||||
# work properly
|
||||
class ChangeCollationForTagNames < ActiveRecord::Migration
|
||||
def up
|
||||
if ActsAsTaggableOn::Utils.using_mysql?
|
||||
execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class RemoveCustomerCodeNotNullConstraint < ActiveRecord::Migration
|
||||
def up
|
||||
change_column :customers, :code, :string, null: true
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :customers, :code, :string, null: false
|
||||
end
|
||||
end
|
||||
16
db/migrate/20150508072938_add_customer_to_orders.rb
Normal file
16
db/migrate/20150508072938_add_customer_to_orders.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class AddCustomerToOrders < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spree_orders, :customer_id, :integer
|
||||
add_index :spree_orders, :customer_id
|
||||
add_foreign_key :spree_orders, :customers, column: :customer_id
|
||||
|
||||
Spree::Order.where("spree_orders.email IS NOT NULL AND distributor_id IS NOT NULL AND customer_id IS NULL").each do |order|
|
||||
customer = Customer.find_by_email_and_enterprise_id(order.email, order.distributor_id)
|
||||
unless customer.present?
|
||||
user = Spree::User.find_by_email(order.email)
|
||||
customer = Customer.create!(email: order.email, enterprise_id: order.distributor_id, user_id: user.andand.id )
|
||||
end
|
||||
order.update_attribute(:customer, customer)
|
||||
end
|
||||
end
|
||||
end
|
||||
27
db/schema.rb
27
db/schema.rb
@@ -158,7 +158,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do
|
||||
create_table "customers", :force => true do |t|
|
||||
t.string "email", :null => false
|
||||
t.integer "enterprise_id", :null => false
|
||||
t.string "code", :null => false
|
||||
t.string "code"
|
||||
t.integer "user_id"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
@@ -621,12 +621,14 @@ ActiveRecord::Schema.define(:version => 20150527004427) do
|
||||
t.string "email"
|
||||
t.text "special_instructions"
|
||||
t.integer "distributor_id"
|
||||
t.integer "order_cycle_id"
|
||||
t.string "currency"
|
||||
t.string "last_ip_address"
|
||||
t.integer "order_cycle_id"
|
||||
t.integer "cart_id"
|
||||
t.integer "customer_id"
|
||||
end
|
||||
|
||||
add_index "spree_orders", ["customer_id"], :name => "index_spree_orders_on_customer_id"
|
||||
add_index "spree_orders", ["number"], :name => "index_orders_on_number"
|
||||
|
||||
create_table "spree_payment_methods", :force => true do |t|
|
||||
@@ -1083,6 +1085,26 @@ ActiveRecord::Schema.define(:version => 20150527004427) do
|
||||
t.integer "state_id"
|
||||
end
|
||||
|
||||
create_table "taggings", :force => true do |t|
|
||||
t.integer "tag_id"
|
||||
t.integer "taggable_id"
|
||||
t.string "taggable_type"
|
||||
t.integer "tagger_id"
|
||||
t.string "tagger_type"
|
||||
t.string "context", :limit => 128
|
||||
t.datetime "created_at"
|
||||
end
|
||||
|
||||
add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], :name => "taggings_idx", :unique => true
|
||||
add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
|
||||
|
||||
create_table "tags", :force => true do |t|
|
||||
t.string "name"
|
||||
t.integer "taggings_count", :default => 0
|
||||
end
|
||||
|
||||
add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true
|
||||
|
||||
create_table "variant_overrides", :force => true do |t|
|
||||
t.integer "variant_id", :null => false
|
||||
t.integer "hub_id", :null => false
|
||||
@@ -1188,6 +1210,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do
|
||||
add_foreign_key "spree_option_values_variants", "spree_variants", name: "spree_option_values_variants_variant_id_fk", column: "variant_id"
|
||||
|
||||
add_foreign_key "spree_orders", "carts", name: "spree_orders_cart_id_fk"
|
||||
add_foreign_key "spree_orders", "customers", name: "spree_orders_customer_id_fk"
|
||||
add_foreign_key "spree_orders", "enterprises", name: "spree_orders_distributor_id_fk", column: "distributor_id"
|
||||
add_foreign_key "spree_orders", "order_cycles", name: "spree_orders_order_cycle_id_fk"
|
||||
add_foreign_key "spree_orders", "spree_addresses", name: "spree_orders_bill_address_id_fk", column: "bill_address_id"
|
||||
|
||||
@@ -128,6 +128,8 @@ module OpenFoodNetwork
|
||||
producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer)
|
||||
permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers)
|
||||
|
||||
hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub)
|
||||
|
||||
# PLUS any variants that are already in an outgoing exchange of this hub, so things don't break
|
||||
# TODO: Remove this when all P-OC are sorted out
|
||||
active_variants = []
|
||||
@@ -135,7 +137,7 @@ module OpenFoodNetwork
|
||||
active_variants = exchange.variants
|
||||
end
|
||||
|
||||
Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants)
|
||||
Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants)
|
||||
else
|
||||
# Any variants produced by MY PRODUCERS that are in this order cycle, where my producer has granted P-OC to the hub
|
||||
producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: managed_participating_producers)
|
||||
@@ -165,6 +167,8 @@ module OpenFoodNetwork
|
||||
producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer)
|
||||
permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers)
|
||||
|
||||
hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub)
|
||||
|
||||
# PLUS any variants that are already in an outgoing exchange of this hub, so things don't break
|
||||
# TODO: Remove this when all P-OC are sorted out
|
||||
active_variants = []
|
||||
@@ -172,7 +176,7 @@ module OpenFoodNetwork
|
||||
active_variants = exchange.variants
|
||||
end
|
||||
|
||||
Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants)
|
||||
Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants)
|
||||
else
|
||||
# Any of my managed producers in this order cycle granted P-OC by the hub
|
||||
granted_producers = related_enterprises_granted(:add_to_order_cycle, by: [hub], scope: managed_participating_producers)
|
||||
|
||||
@@ -47,7 +47,7 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def variants
|
||||
filter(child_variants) + filter(master_variants)
|
||||
filter(child_variants)
|
||||
end
|
||||
|
||||
def child_variants
|
||||
@@ -57,16 +57,6 @@ module OpenFoodNetwork
|
||||
.order("spree_products.name")
|
||||
end
|
||||
|
||||
def master_variants
|
||||
Spree::Variant.where(:is_master => true)
|
||||
.joins(:product)
|
||||
.where("(select spree_variants.id from spree_variants as other_spree_variants
|
||||
WHERE other_spree_variants.product_id = spree_variants.product_id
|
||||
AND other_spree_variants.is_master = 'f' LIMIT 1) IS NULL")
|
||||
.merge(visible_products)
|
||||
.order("spree_products.name")
|
||||
end
|
||||
|
||||
def filter(variants)
|
||||
# NOTE: Ordering matters.
|
||||
# filter_to_order_cycle and filter_to_distributor return Arrays not Arel
|
||||
@@ -107,7 +97,7 @@ module OpenFoodNetwork
|
||||
def filter_to_order_cycle(variants)
|
||||
if params[:order_cycle_id].to_i > 0
|
||||
order_cycle = OrderCycle.find params[:order_cycle_id]
|
||||
variants.select! { |v| order_cycle.variants.include? v }
|
||||
variants.select { |v| order_cycle.variants.include? v }
|
||||
else
|
||||
variants
|
||||
end
|
||||
|
||||
@@ -13,7 +13,8 @@ echo "--- Bundling"
|
||||
bundle install
|
||||
|
||||
echo "--- Loading test database"
|
||||
bundle exec rake db:test:load
|
||||
bundle exec rake db:drop db:create db:schema:load
|
||||
bundle exec rake parallel:drop parallel:create parallel:load_schema
|
||||
|
||||
echo "--- Running tests"
|
||||
bundle exec rspec --tag ~performance spec
|
||||
bundle exec rake parallel:spec
|
||||
|
||||
95
spec/controllers/admin/customers_controller_spec.rb
Normal file
95
spec/controllers/admin/customers_controller_spec.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
describe Admin::CustomersController, type: :controller do
|
||||
include AuthenticationWorkflow
|
||||
|
||||
describe "index" do
|
||||
let(:enterprise) { create(:distributor_enterprise) }
|
||||
let(:another_enterprise) { create(:distributor_enterprise) }
|
||||
|
||||
context "html" do
|
||||
before do
|
||||
controller.stub spree_current_user: enterprise.owner
|
||||
end
|
||||
|
||||
it "returns an empty @collection" do
|
||||
spree_get :index, format: :html
|
||||
expect(assigns(:collection)).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "json" do
|
||||
let!(:customer) { create(:customer, enterprise: enterprise) }
|
||||
|
||||
context "where I manage the enterprise" do
|
||||
before do
|
||||
controller.stub spree_current_user: enterprise.owner
|
||||
end
|
||||
|
||||
context "and enterprise_id is given in params" do
|
||||
let(:params) { { format: :json, enterprise_id: enterprise.id } }
|
||||
|
||||
it "scopes @collection to customers of that enterprise" do
|
||||
spree_get :index, params
|
||||
expect(assigns(:collection)).to eq [customer]
|
||||
end
|
||||
|
||||
it "serializes the data" do
|
||||
expect(ActiveModel::ArraySerializer).to receive(:new)
|
||||
spree_get :index, params
|
||||
end
|
||||
end
|
||||
|
||||
context "and enterprise_id is not given in params" do
|
||||
it "returns an empty collection" do
|
||||
spree_get :index, format: :json
|
||||
expect(assigns(:collection)).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "and I do not manage the enterprise" do
|
||||
before do
|
||||
controller.stub spree_current_user: another_enterprise.owner
|
||||
end
|
||||
|
||||
it "returns an empty collection" do
|
||||
spree_get :index, format: :json
|
||||
expect(assigns(:collection)).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "update" do
|
||||
let(:enterprise) { create(:distributor_enterprise) }
|
||||
let(:another_enterprise) { create(:distributor_enterprise) }
|
||||
|
||||
context "json" do
|
||||
let!(:customer) { create(:customer, enterprise: enterprise) }
|
||||
|
||||
context "where I manage the customer's enterprise" do
|
||||
before do
|
||||
controller.stub spree_current_user: enterprise.owner
|
||||
end
|
||||
|
||||
it "allows me to update the customer" do
|
||||
spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' }
|
||||
expect(assigns(:customer)).to eq customer
|
||||
expect(customer.reload.email).to eq 'new.email@gmail.com'
|
||||
end
|
||||
end
|
||||
|
||||
context "where I don't manage the customer's enterprise" do
|
||||
before do
|
||||
controller.stub spree_current_user: another_enterprise.owner
|
||||
end
|
||||
|
||||
it "prevents me from updating the customer" do
|
||||
spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' }
|
||||
expect(response).to redirect_to spree.unauthorized_path
|
||||
expect(assigns(:customer)).to eq nil
|
||||
expect(customer.email).to_not eq 'new.email@gmail.com'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -212,7 +212,7 @@ FactoryGirl.define do
|
||||
factory :customer, :class => Customer do
|
||||
email { Faker::Internet.email }
|
||||
enterprise
|
||||
code 'abc123'
|
||||
code { Faker::Lorem.word }
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
71
spec/features/admin/customers_spec.rb
Normal file
71
spec/features/admin/customers_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require 'spec_helper'
|
||||
|
||||
feature 'Customers' do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
context "as an enterprise user" do
|
||||
let(:user) { create_enterprise_user }
|
||||
let(:managed_distributor) { create(:distributor_enterprise, owner: user) }
|
||||
let(:unmanaged_distributor) { create(:distributor_enterprise) }
|
||||
|
||||
describe "using the customers index" do
|
||||
let!(:customer1) { create(:customer, enterprise: managed_distributor) }
|
||||
let!(:customer2) { create(:customer, enterprise: managed_distributor) }
|
||||
let!(:customer3) { create(:customer, enterprise: unmanaged_distributor) }
|
||||
|
||||
before do
|
||||
quick_login_as user
|
||||
visit admin_customers_path
|
||||
end
|
||||
|
||||
it "passes the smoke test", js: true do
|
||||
# Prompts for a hub for a list of my managed enterprises
|
||||
expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name]
|
||||
|
||||
select2_select managed_distributor.name, from: "shop_id"
|
||||
|
||||
# Loads the right customers
|
||||
expect(page).to have_selector "tr#c_#{customer1.id}"
|
||||
expect(page).to have_selector "tr#c_#{customer2.id}"
|
||||
expect(page).to_not have_selector "tr#c_#{customer3.id}"
|
||||
|
||||
# Searching
|
||||
fill_in "quick_search", with: customer2.email
|
||||
expect(page).to_not have_selector "tr#c_#{customer1.id}"
|
||||
expect(page).to have_selector "tr#c_#{customer2.id}"
|
||||
fill_in "quick_search", with: ""
|
||||
|
||||
# Toggling columns
|
||||
expect(page).to have_selector "th.email"
|
||||
expect(page).to have_content customer1.email
|
||||
first("div#columns_dropdown", :text => "COLUMNS").click
|
||||
first("div#columns_dropdown div.menu div.menu_item", text: "Email").click
|
||||
expect(page).to_not have_selector "th.email"
|
||||
expect(page).to_not have_content customer1.email
|
||||
end
|
||||
|
||||
it "allows updating of attributes", js: true do
|
||||
select2_select managed_distributor.name, from: "shop_id"
|
||||
|
||||
within "tr#c_#{customer1.id}" do
|
||||
fill_in "code", with: "new-customer-code"
|
||||
expect(page).to have_css "input#code.update-pending"
|
||||
end
|
||||
within "tr#c_#{customer1.id}" do
|
||||
find(:css, "tags-input .tags input").set "awesome\n"
|
||||
expect(page).to have_css ".tag_watcher.update-pending"
|
||||
end
|
||||
click_button "Update"
|
||||
|
||||
# Every says it updated
|
||||
expect(page).to have_css "input#code.update-success"
|
||||
expect(page).to have_css ".tag_watcher.update-success"
|
||||
|
||||
# And it actually did
|
||||
expect(customer1.reload.code).to eq "new-customer-code"
|
||||
expect(customer1.tag_list).to eq ["awesome"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,7 @@
|
||||
//= require angularjs-file-upload
|
||||
//= require lodash.underscore.js
|
||||
//= require angular-flash.min.js
|
||||
//= require shared/ng-tags-input.min.js
|
||||
//= require shared/mm-foundation-tpls-0.2.2.min.js
|
||||
//= require textAngular.min.js
|
||||
//= require textAngular-sanitize.min.js
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
describe "CustomersCtrl", ->
|
||||
ctrl = null
|
||||
scope = null
|
||||
Customers = null
|
||||
|
||||
beforeEach ->
|
||||
shops = "list of shops"
|
||||
|
||||
module('admin.customers')
|
||||
inject ($controller, $rootScope, _Customers_) ->
|
||||
scope = $rootScope
|
||||
Customers = _Customers_
|
||||
ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops}
|
||||
|
||||
describe "setting the shop on scope", ->
|
||||
beforeEach ->
|
||||
spyOn(Customers, "index").andReturn "list of customers"
|
||||
scope.$apply ->
|
||||
scope.shop = {id: 1}
|
||||
|
||||
it "calls Customers#index with the correct params", ->
|
||||
expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1})
|
||||
|
||||
it "resets $scope.customers with the result of Customers#index", ->
|
||||
expect(scope.customers).toEqual "list of customers"
|
||||
@@ -0,0 +1,31 @@
|
||||
describe "Customers service", ->
|
||||
Customers = CustomerResource = customers = $httpBackend = null
|
||||
|
||||
beforeEach ->
|
||||
module 'admin.customers'
|
||||
|
||||
inject ($q, _$httpBackend_, _Customers_, _CustomerResource_) ->
|
||||
Customers = _Customers_
|
||||
CustomerResource = _CustomerResource_
|
||||
$httpBackend = _$httpBackend_
|
||||
$httpBackend.expectGET('/admin/customers.json?enterprise_id=2').respond 200, [{ id: 5, email: 'someone@email.com'}]
|
||||
|
||||
describe "#index", ->
|
||||
result = null
|
||||
|
||||
beforeEach ->
|
||||
expect(Customers.loaded).toBe false
|
||||
result = Customers.index(enterprise_id: 2)
|
||||
$httpBackend.flush()
|
||||
|
||||
it "stores returned data in @customers, with ids as keys", ->
|
||||
# This is super weird and freaking annoying. I think resource results have extra
|
||||
# properties ($then, $promise) that cause them to not be equal to the reponse object
|
||||
# provided to the expectGET clause above.
|
||||
expect(Customers.customers).toEqual [ new CustomerResource({ id: 5, email: 'someone@email.com'}) ]
|
||||
|
||||
it "returns @customers", ->
|
||||
expect(result).toEqual Customers.customers
|
||||
|
||||
it "sets @loaded to true", ->
|
||||
expect(Customers.loaded).toBe true
|
||||
@@ -0,0 +1,17 @@
|
||||
describe "ColumnsCtrl", ->
|
||||
ctrl = null
|
||||
scope = null
|
||||
Columns = null
|
||||
|
||||
beforeEach ->
|
||||
Columns = { columns: { name: { visible: true} } }
|
||||
|
||||
module('admin.indexUtils')
|
||||
inject ($controller, $rootScope) ->
|
||||
scope = $rootScope
|
||||
ctrl = $controller 'ColumnsCtrl', {$scope: scope, Columns: Columns}
|
||||
|
||||
it "initialises data", ->
|
||||
expect(scope.columns).toEqual Columns.columns
|
||||
expect(scope.predicate).toEqual ""
|
||||
expect(scope.reverse).toEqual false
|
||||
@@ -0,0 +1,15 @@
|
||||
describe "Columns service", ->
|
||||
Columns = null
|
||||
|
||||
beforeEach ->
|
||||
module 'admin.indexUtils'
|
||||
|
||||
inject (_Columns_) ->
|
||||
Columns = _Columns_
|
||||
|
||||
Columns.columns = ["something"]
|
||||
|
||||
describe "setting columns", ->
|
||||
it "sets resets @columns and copies each column of the provided object across", ->
|
||||
Columns.setColumns({ name: { visible: true } })
|
||||
expect(Columns.columns).toEqual { name: { visible: true } }
|
||||
@@ -0,0 +1,149 @@
|
||||
describe "Pending Changes", ->
|
||||
resourcesMock = pendingChanges = null
|
||||
|
||||
beforeEach ->
|
||||
|
||||
resourcesMock =
|
||||
update: jasmine.createSpy('update').andCallFake (change) ->
|
||||
$promise:
|
||||
then: (successFn, errorFn) ->
|
||||
return successFn({propertyName: "new_value"}) if change.success
|
||||
errorFn("error")
|
||||
|
||||
module 'admin.indexUtils', ($provide) ->
|
||||
$provide.value 'resources', resourcesMock
|
||||
return
|
||||
|
||||
inject (_pendingChanges_) ->
|
||||
pendingChanges = _pendingChanges_
|
||||
|
||||
|
||||
describe "adding a new change", ->
|
||||
it "adds a new object with key of id if it does not already exist", ->
|
||||
expect(pendingChanges.pendingChanges).toEqual {}
|
||||
expect(pendingChanges.pendingChanges["1"]).not.toBeDefined()
|
||||
pendingChanges.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChanges.pendingChanges["1"]).toBeDefined()
|
||||
|
||||
it "adds a new object with key of the altered attribute name if it does not already exist", ->
|
||||
pendingChanges.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChanges.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 }
|
||||
|
||||
it "replaces the existing object when adding a change to an attribute which already exists", ->
|
||||
pendingChanges.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChanges.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 }
|
||||
pendingChanges.add 1, "propertyName", { b: 2 }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { b: 2 }
|
||||
|
||||
it "adds an attribute to key to a line item object when one already exists", ->
|
||||
pendingChanges.add 1, "propertyName1", { a: 1 }
|
||||
pendingChanges.add 1, "propertyName2", { b: 2 }
|
||||
expect(pendingChanges.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } }
|
||||
|
||||
describe "removing all existing changes", ->
|
||||
it "resets pendingChanges object", ->
|
||||
pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName2"]).toBeDefined()
|
||||
pendingChanges.removeAll()
|
||||
expect(pendingChanges.pendingChanges["1"]).not.toBeDefined()
|
||||
expect(pendingChanges.pendingChanges).toEqual {}
|
||||
|
||||
describe "removing an existing change", ->
|
||||
it "deletes a change if it exists", ->
|
||||
pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChanges.remove 1, "propertyName1"
|
||||
expect(pendingChanges.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).not.toBeDefined()
|
||||
|
||||
it "deletes a line item object if it is empty", ->
|
||||
pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChanges.remove 1, "propertyName1"
|
||||
expect(pendingChanges.pendingChanges["1"]).not.toBeDefined()
|
||||
|
||||
it "does nothing if key with specified attribute does not exist", ->
|
||||
pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChanges.remove 1, "propertyName2"
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 }
|
||||
|
||||
it "does nothing if key with specified id does not exist", ->
|
||||
pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChanges.remove 2, "propertyName1"
|
||||
expect(pendingChanges.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } }
|
||||
|
||||
describe "submitting an individual change to the server", ->
|
||||
change = null
|
||||
beforeEach ->
|
||||
object = {id: 1}
|
||||
scope = { reset: jasmine.createSpy('reset'), success: jasmine.createSpy('success'), error: jasmine.createSpy('error') };
|
||||
attr = "propertyName"
|
||||
change = { object: object, scope: scope, attr: attr }
|
||||
|
||||
|
||||
it "sends the correct object to dataSubmitter", ->
|
||||
pendingChanges.submit change
|
||||
expect(resourcesMock.update.calls.length).toEqual 1
|
||||
expect(resourcesMock.update).toHaveBeenCalledWith change
|
||||
|
||||
describe "successful request", ->
|
||||
beforeEach ->
|
||||
change.success = true
|
||||
|
||||
it "calls remove with id and attribute name", ->
|
||||
spyOn(pendingChanges, "remove").andCallFake(->)
|
||||
pendingChanges.submit change
|
||||
expect(pendingChanges.remove.calls.length).toEqual 1
|
||||
expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName"
|
||||
|
||||
it "calls reset on the relevant scope", ->
|
||||
pendingChanges.submit change
|
||||
expect(change.scope.reset).toHaveBeenCalledWith "new_value"
|
||||
|
||||
it "calls success on the relevant scope", ->
|
||||
pendingChanges.submit change
|
||||
expect(change.scope.success).toHaveBeenCalled()
|
||||
|
||||
describe "unsuccessful request", ->
|
||||
beforeEach ->
|
||||
change.success = false
|
||||
|
||||
it "does not call remove", ->
|
||||
spyOn(pendingChanges, "remove").andCallFake(->)
|
||||
pendingChanges.submit change
|
||||
expect(pendingChanges.remove).not.toHaveBeenCalled()
|
||||
|
||||
it "does not call reset on the relevant scope", ->
|
||||
pendingChanges.submit change
|
||||
expect(change.scope.reset).not.toHaveBeenCalled()
|
||||
|
||||
it "calls error on the relevant scope", ->
|
||||
pendingChanges.submit change
|
||||
expect(change.scope.error).toHaveBeenCalled()
|
||||
|
||||
describe "cycling through all changes to submit to server", ->
|
||||
it "sends the correct object to dataSubmitter", ->
|
||||
spyOn(pendingChanges, "submit").andCallFake(->)
|
||||
pendingChanges.pendingChanges =
|
||||
1: { "prop1": { attr: "prop1", value: 1 }, "prop2": { attr: "prop2", value: 2 } }
|
||||
2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop2", value: 4 } }
|
||||
7: { "prop2": { attr: "prop2", value: 5 } }
|
||||
pendingChanges.submitAll()
|
||||
expect(pendingChanges.submit.calls.length).toEqual 5
|
||||
expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 1 }
|
||||
expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 2 }
|
||||
expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 2 }
|
||||
expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 4 }
|
||||
expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 5 }
|
||||
|
||||
it "returns an array of promises representing all sumbit requests", ->
|
||||
spyOn(pendingChanges, "submit").andCallFake (change) -> change.value
|
||||
pendingChanges.pendingChanges =
|
||||
1: { "prop1": { attr: "prop1", value: 1 } }
|
||||
2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop1", value: 4 } }
|
||||
expect(pendingChanges.submitAll()).toEqual [ 1, 2, 4 ]
|
||||
@@ -0,0 +1,52 @@
|
||||
describe "switchClass service", ->
|
||||
elementMock = timeoutMock = {}
|
||||
removeClass = addClass = switchClassService = null
|
||||
|
||||
beforeEach ->
|
||||
addClass = jasmine.createSpy('addClass')
|
||||
removeClass = jasmine.createSpy('removeClass')
|
||||
elementMock =
|
||||
addClass: addClass
|
||||
removeClass: removeClass
|
||||
timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout"
|
||||
timeoutMock.cancel = jasmine.createSpy('timeout.cancel')
|
||||
|
||||
beforeEach ->
|
||||
module "ofn.admin" , ($provide) ->
|
||||
$provide.value '$timeout', timeoutMock
|
||||
return
|
||||
|
||||
beforeEach inject (switchClass) ->
|
||||
switchClassService = switchClass
|
||||
|
||||
it "calls addClass on the element once", ->
|
||||
switchClassService elementMock, "addClass", [], false
|
||||
expect(addClass).toHaveBeenCalledWith "addClass"
|
||||
expect(addClass.calls.length).toEqual 1
|
||||
|
||||
it "calls removeClass on the element for ", ->
|
||||
switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass1"
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass2"
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass3"
|
||||
expect(removeClass.calls.length).toEqual 3
|
||||
|
||||
it "call cancel on element.timout only if it exists", ->
|
||||
switchClassService elementMock, "", [], false
|
||||
expect(timeoutMock.cancel).not.toHaveBeenCalled()
|
||||
elementMock.timeout = true
|
||||
switchClassService elementMock, "", [], false
|
||||
expect(timeoutMock.cancel).toHaveBeenCalled()
|
||||
|
||||
it "doesn't set up a new timeout if 'timeout' is false", ->
|
||||
switchClassService elementMock, "class1", ["class2"], false
|
||||
expect(timeoutMock).not.toHaveBeenCalled()
|
||||
|
||||
it "doesn't set up a new timeout if 'timeout' is a string", ->
|
||||
switchClassService elementMock, "class1", ["class2"], "string"
|
||||
expect(timeoutMock).not.toHaveBeenCalled()
|
||||
|
||||
it "sets up a new timeout if 'timeout' parameter is an integer", ->
|
||||
switchClassService elementMock, "class1", ["class2"], 1000
|
||||
expect(timeoutMock).toHaveBeenCalled()
|
||||
expect(elementMock.timeout).toEqual "new timeout"
|
||||
@@ -376,236 +376,6 @@ describe "AdminOrderMgmtCtrl", ->
|
||||
sp = scope.filteredLineItems[0].price
|
||||
expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp
|
||||
|
||||
|
||||
describe "managing pending changes", ->
|
||||
dataSubmitter = pendingChangesService = null
|
||||
|
||||
beforeEach ->
|
||||
dataSubmitter = jasmine.createSpy('dataSubmitter').andReturn {
|
||||
then: (thenFn) ->
|
||||
thenFn({propertyName: "new_value"})
|
||||
}
|
||||
|
||||
beforeEach ->
|
||||
module "ofn.admin", ($provide) ->
|
||||
$provide.value 'dataSubmitter', dataSubmitter
|
||||
return
|
||||
|
||||
beforeEach inject (pendingChanges) ->
|
||||
pendingChangesService = pendingChanges
|
||||
|
||||
describe "adding a new change", ->
|
||||
it "adds a new object with key of id if it does not already exist", ->
|
||||
expect(pendingChangesService.pendingChanges).toEqual {}
|
||||
expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined()
|
||||
pendingChangesService.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChangesService.pendingChanges["1"]).toBeDefined()
|
||||
|
||||
it "adds a new object with key of the altered attribute name if it does not already exist", ->
|
||||
pendingChangesService.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChangesService.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 }
|
||||
|
||||
it "replaces the existing object when adding a change to an attribute which already exists", ->
|
||||
pendingChangesService.add 1, "propertyName", { a: 1 }
|
||||
expect(pendingChangesService.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 }
|
||||
pendingChangesService.add 1, "propertyName", { b: 2 }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { b: 2 }
|
||||
|
||||
it "adds an attribute to key to a line item object when one already exists", ->
|
||||
pendingChangesService.add 1, "propertyName1", { a: 1 }
|
||||
pendingChangesService.add 1, "propertyName2", { b: 2 }
|
||||
expect(pendingChangesService.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } }
|
||||
|
||||
describe "removing all existing changes", ->
|
||||
it "resets pendingChanges object", ->
|
||||
pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName2"]).toBeDefined()
|
||||
pendingChangesService.removeAll()
|
||||
expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined()
|
||||
expect(pendingChangesService.pendingChanges).toEqual {}
|
||||
|
||||
describe "removing an existing change", ->
|
||||
it "deletes a change if it exists", ->
|
||||
pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChangesService.remove 1, "propertyName1"
|
||||
expect(pendingChangesService.pendingChanges["1"]).toBeDefined()
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).not.toBeDefined()
|
||||
|
||||
it "deletes a line item object if it is empty", ->
|
||||
pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChangesService.remove 1, "propertyName1"
|
||||
expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined()
|
||||
|
||||
it "does nothing if key with specified attribute does not exist", ->
|
||||
pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChangesService.remove 1, "propertyName2"
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 }
|
||||
|
||||
it "does nothing if key with specified id does not exist", ->
|
||||
pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } }
|
||||
expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined()
|
||||
pendingChangesService.remove 2, "propertyName1"
|
||||
expect(pendingChangesService.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } }
|
||||
|
||||
describe "submitting an individual change to the server", ->
|
||||
it "sends the correct object to dataSubmitter", ->
|
||||
changeObj = { element: {} }
|
||||
pendingChangesService.submit 1, "propertyName", changeObj
|
||||
expect(dataSubmitter.calls.length).toEqual 1
|
||||
expect(dataSubmitter).toHaveBeenCalledWith changeObj
|
||||
|
||||
it "calls remove with id and attribute name", ->
|
||||
changeObj = { element: {} }
|
||||
spyOn(pendingChangesService, "remove").andCallFake(->)
|
||||
pendingChangesService.submit 1, "propertyName", changeObj
|
||||
expect(pendingChangesService.remove.calls.length).toEqual 1
|
||||
expect(pendingChangesService.remove).toHaveBeenCalledWith 1, "propertyName"
|
||||
|
||||
it "resets the dbValue attribute of the element in question", ->
|
||||
element = { dbValue: 2 }
|
||||
changeObj = { element: element }
|
||||
pendingChangesService.submit 1, "propertyName", changeObj
|
||||
expect(element.dbValue).toEqual "new_value"
|
||||
|
||||
describe "cycling through all changes to submit to server", ->
|
||||
it "sends the correct object to dataSubmitter", ->
|
||||
spyOn(pendingChangesService, "submit").andCallFake(->)
|
||||
pendingChangesService.pendingChanges =
|
||||
1: { "prop1": 1, "prop2": 2 }
|
||||
2: { "prop1": 2, "prop2": 4 }
|
||||
7: { "prop2": 5 }
|
||||
pendingChangesService.submitAll()
|
||||
expect(pendingChangesService.submit.calls.length).toEqual 5
|
||||
expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop1", 1
|
||||
expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop2", 2
|
||||
expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop1", 2
|
||||
expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop2", 4
|
||||
expect(pendingChangesService.submit).toHaveBeenCalledWith '7', "prop2", 5
|
||||
|
||||
it "returns an array of promises representing all sumbit requests", ->
|
||||
spyOn(pendingChangesService, "submit").andCallFake (id,attrName,changeObj) ->
|
||||
id
|
||||
pendingChangesService.pendingChanges =
|
||||
1: { "prop1": 1 }
|
||||
2: { "prop1": 2, "prop2": 4 }
|
||||
expect(pendingChangesService.submitAll()).toEqual [ '1','2','2' ]
|
||||
|
||||
describe "dataSubmitter service", ->
|
||||
qMock = httpMock = {}
|
||||
switchClassSpy = resolveSpy = rejectSpy = dataSubmitterService = null
|
||||
|
||||
beforeEach ->
|
||||
resolveSpy = jasmine.createSpy('resolve')
|
||||
rejectSpy = jasmine.createSpy('reject')
|
||||
qMock.defer = ->
|
||||
resolve: resolveSpy
|
||||
reject: rejectSpy
|
||||
promise: "promise1"
|
||||
|
||||
# Can't use httpBackend because the qMock interferes with it
|
||||
httpMock.put = (url) ->
|
||||
success: (successFn) ->
|
||||
successFn("somedata") if url == "successURL"
|
||||
error: (errorFn) ->
|
||||
errorFn() if url == "errorURL"
|
||||
|
||||
spyOn(httpMock, "put").andCallThrough()
|
||||
spyOn(qMock, "defer").andCallThrough()
|
||||
|
||||
switchClassSpy = jasmine.createSpy('switchClass')
|
||||
|
||||
beforeEach ->
|
||||
module "ofn.admin" , ($provide) ->
|
||||
$provide.value '$q', qMock
|
||||
$provide.value '$http', httpMock
|
||||
$provide.value 'switchClass', switchClassSpy
|
||||
return
|
||||
|
||||
beforeEach inject (dataSubmitter) ->
|
||||
dataSubmitterService = dataSubmitter
|
||||
|
||||
it "returns a promise", ->
|
||||
expect(dataSubmitterService( { url: "successURL" } )).toEqual "promise1"
|
||||
expect(qMock.defer).toHaveBeenCalled()
|
||||
|
||||
it "sends a PUT request with the url property of changeObj", ->
|
||||
dataSubmitterService { url: "successURL" }
|
||||
expect(httpMock.put).toHaveBeenCalledWith "successURL"
|
||||
|
||||
it "calls resolve on deferred object when request is successful", ->
|
||||
element = { a: 1 }
|
||||
dataSubmitterService { url: "successURL", element: element }
|
||||
expect(resolveSpy.calls.length).toEqual 1
|
||||
expect(rejectSpy.calls.length).toEqual 0
|
||||
expect(resolveSpy).toHaveBeenCalledWith "somedata"
|
||||
expect(switchClassSpy).toHaveBeenCalledWith element, "update-success", ["update-pending", "update-error"], 3000
|
||||
|
||||
it "calls reject on deferred object when request is erroneous", ->
|
||||
element = { b: 2 }
|
||||
dataSubmitterService { url: "errorURL", element: element }
|
||||
expect(resolveSpy.calls.length).toEqual 0
|
||||
expect(rejectSpy.calls.length).toEqual 1
|
||||
expect(switchClassSpy).toHaveBeenCalledWith element, "update-error", ["update-pending", "update-success"], false
|
||||
|
||||
describe "switchClass service", ->
|
||||
elementMock = timeoutMock = {}
|
||||
removeClass = addClass = switchClassService = null
|
||||
|
||||
beforeEach ->
|
||||
addClass = jasmine.createSpy('addClass')
|
||||
removeClass = jasmine.createSpy('removeClass')
|
||||
elementMock =
|
||||
addClass: addClass
|
||||
removeClass: removeClass
|
||||
timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout"
|
||||
timeoutMock.cancel = jasmine.createSpy('timeout.cancel')
|
||||
|
||||
beforeEach ->
|
||||
module "ofn.admin" , ($provide) ->
|
||||
$provide.value '$timeout', timeoutMock
|
||||
return
|
||||
|
||||
beforeEach inject (switchClass) ->
|
||||
switchClassService = switchClass
|
||||
|
||||
it "calls addClass on the element once", ->
|
||||
switchClassService elementMock, "addClass", [], false
|
||||
expect(addClass).toHaveBeenCalledWith "addClass"
|
||||
expect(addClass.calls.length).toEqual 1
|
||||
|
||||
it "calls removeClass on the element for ", ->
|
||||
switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass1"
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass2"
|
||||
expect(removeClass).toHaveBeenCalledWith "remClass3"
|
||||
expect(removeClass.calls.length).toEqual 3
|
||||
|
||||
it "call cancel on element.timout only if it exists", ->
|
||||
switchClassService elementMock, "", [], false
|
||||
expect(timeoutMock.cancel).not.toHaveBeenCalled()
|
||||
elementMock.timeout = true
|
||||
switchClassService elementMock, "", [], false
|
||||
expect(timeoutMock.cancel).toHaveBeenCalled()
|
||||
|
||||
it "doesn't set up a new timeout if 'timeout' is false", ->
|
||||
switchClassService elementMock, "class1", ["class2"], false
|
||||
expect(timeoutMock).not.toHaveBeenCalled()
|
||||
|
||||
it "doesn't set up a new timeout if 'timeout' is a string", ->
|
||||
switchClassService elementMock, "class1", ["class2"], "string"
|
||||
expect(timeoutMock).not.toHaveBeenCalled()
|
||||
|
||||
it "sets up a new timeout if 'timeout' parameter is an integer", ->
|
||||
switchClassService elementMock, "class1", ["class2"], 1000
|
||||
expect(timeoutMock).toHaveBeenCalled()
|
||||
expect(elementMock.timeout).toEqual "new timeout"
|
||||
|
||||
describe "Auxiliary functions", ->
|
||||
describe "getting a zero filled two digit number", ->
|
||||
it "returns the number as a string if its value is greater than or equal to 10", ->
|
||||
|
||||
@@ -545,6 +545,16 @@ module OpenFoodNetwork
|
||||
expect(visible).to_not include v2
|
||||
end
|
||||
|
||||
context "where the hub produces products" do
|
||||
# NOTE: No relationship to self required
|
||||
let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) }
|
||||
|
||||
it "returns any variants produced by the hub" do
|
||||
visible = permissions.visible_variants_for_outgoing_exchanges_to(hub)
|
||||
expect(visible).to include v3
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: for backwards compatability, remove later
|
||||
context "when an exchange exists between the coordinator and the hub within this order cycle" do
|
||||
let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) }
|
||||
@@ -712,6 +722,16 @@ module OpenFoodNetwork
|
||||
expect(visible).to_not include v2
|
||||
end
|
||||
|
||||
context "where the hub produces products" do
|
||||
# NOTE: No relationship to self required
|
||||
let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) }
|
||||
|
||||
it "returns any variants produced by the hub" do
|
||||
visible = permissions.visible_variants_for_outgoing_exchanges_to(hub)
|
||||
expect(visible).to include v3
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: for backwards compatability, remove later
|
||||
context "when an exchange exists between the coordinator and the hub within this order cycle" do
|
||||
let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) }
|
||||
|
||||
@@ -54,10 +54,8 @@ module OpenFoodNetwork
|
||||
|
||||
it "fetches variants for some params" do
|
||||
subject.should_receive(:child_variants).and_return ["children"]
|
||||
subject.should_receive(:master_variants).and_return ["masters"]
|
||||
subject.should_receive(:filter).with(['children']).and_return ["filter_children"]
|
||||
subject.should_receive(:filter).with(['masters']).and_return ["filter_masters"]
|
||||
subject.variants.should == ["filter_children", "filter_masters"]
|
||||
subject.variants.should == ["filter_children"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -92,14 +90,6 @@ module OpenFoodNetwork
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetching master variants" do
|
||||
it "doesn't return master variants with siblings" do
|
||||
product = create(:simple_product, supplier: supplier)
|
||||
|
||||
subject.master_variants.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "Filtering variants" do
|
||||
let(:variants) { Spree::Variant.scoped.joins(:product).where(is_master: false) }
|
||||
it "should return unfiltered variants sans-params" do
|
||||
|
||||
20
spec/models/customer_spec.rb
Normal file
20
spec/models/customer_spec.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Customer, type: :model do
|
||||
describe 'creation callbacks' do
|
||||
let!(:user1) { create(:user) }
|
||||
let!(:user2) { create(:user) }
|
||||
let!(:enterprise) { create(:distributor_enterprise) }
|
||||
|
||||
it "associates an existing user using email" do
|
||||
c1 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com')
|
||||
expect(c1.user).to be_nil
|
||||
|
||||
c2 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com', user: user1)
|
||||
expect(c2.user).to eq user1
|
||||
|
||||
c3 = Customer.create(enterprise: enterprise, email: user2.email)
|
||||
expect(c3.user).to eq user2
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -121,14 +121,18 @@ describe Enterprise do
|
||||
e.relatives.should match_array [p, c]
|
||||
end
|
||||
|
||||
it "finds relatives_including_self" do
|
||||
expect(e.relatives_including_self).to include e
|
||||
end
|
||||
|
||||
it "scopes relatives to visible distributors" do
|
||||
e.should_receive(:relatives).and_return(relatives = [])
|
||||
e.should_receive(:relatives_including_self).and_return(relatives = [])
|
||||
relatives.should_receive(:is_distributor).and_return relatives
|
||||
e.distributors
|
||||
end
|
||||
|
||||
it "scopes relatives to visible producers" do
|
||||
e.should_receive(:relatives).and_return(relatives = [])
|
||||
e.should_receive(:relatives_including_self).and_return(relatives = [])
|
||||
relatives.should_receive(:is_primary_producer).and_return relatives
|
||||
e.suppliers
|
||||
end
|
||||
|
||||
@@ -220,6 +220,10 @@ module Spree
|
||||
should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report)
|
||||
end
|
||||
|
||||
it "should not be able to access customer actions" do
|
||||
should_not have_ability([:admin, :index, :update], for: Customer)
|
||||
end
|
||||
|
||||
describe "order_cycles abilities" do
|
||||
context "where the enterprise is not in an order_cycle" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
@@ -407,6 +411,10 @@ module Spree
|
||||
should_not have_ability([:sales_total, :users_and_enterprises], for: :report)
|
||||
end
|
||||
|
||||
it "should be able to access customer actions" do
|
||||
should have_ability([:admin, :index, :update], for: Customer)
|
||||
end
|
||||
|
||||
context "for a given order_cycle" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:exchange){ create(:exchange, incoming: false, order_cycle: order_cycle, receiver: d1, sender: order_cycle.coordinator) }
|
||||
|
||||
@@ -511,4 +511,43 @@ describe Spree::Order do
|
||||
end.to enqueue_job ConfirmOrderJob
|
||||
end
|
||||
end
|
||||
|
||||
describe "associating a customer" do
|
||||
let(:user) { create(:user) }
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
|
||||
context "when a user has been set on the order" do
|
||||
let!(:order) { create(:order, distributor: distributor, user: user) }
|
||||
context "and a customer for order.distributor and order.user.email already exists" do
|
||||
let!(:customer) { create(:customer, enterprise: distributor, email: user.email) }
|
||||
it "associates the order with the existing customer" do
|
||||
order.send(:associate_customer)
|
||||
expect(order.customer).to eq customer
|
||||
end
|
||||
end
|
||||
context "and a customer for order.distributor and order.user.email does not alread exist" do
|
||||
let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') }
|
||||
it "creates a new customer" do
|
||||
expect{order.send(:associate_customer)}.to change{Customer.count}.by 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user has not been set on the order" do
|
||||
let!(:order) { create(:order, distributor: distributor, user: nil) }
|
||||
context "and a customer for order.distributor and order.email already exists" do
|
||||
let!(:customer) { create(:customer, enterprise: distributor, email: order.email) }
|
||||
it "creates a new customer" do
|
||||
order.send(:associate_customer)
|
||||
expect(order.customer).to eq customer
|
||||
end
|
||||
end
|
||||
context "and a customer for order.distributor and order.email does not alread exist" do
|
||||
let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') }
|
||||
it "creates a new customer" do
|
||||
expect{order.send(:associate_customer)}.to change{Customer.count}.by 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,6 +21,7 @@ RSpec::Matchers.define :have_select2 do |id, options={}|
|
||||
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
|
||||
results << no_options_present(from, options[:without_options]) if options.key? :without_options
|
||||
end
|
||||
|
||||
results.all?
|
||||
@@ -51,6 +52,14 @@ RSpec::Matchers.define :have_select2 do |id, options={}|
|
||||
end
|
||||
end
|
||||
|
||||
def no_options_present(from, options)
|
||||
with_select2_open(from) do
|
||||
options.none? do |option|
|
||||
@node.has_selector? "div.select2-drop-active ul.select2-results li", text: option
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def selected_option_is(from, text)
|
||||
within find(from) do
|
||||
find("a.select2-choice").text == text
|
||||
|
||||
Reference in New Issue
Block a user