Merge branch 'master' into optimise-shopfront

This commit is contained in:
Rohan Mitchell
2015-06-05 07:28:05 +10:00
82 changed files with 1149 additions and 429 deletions

3
.rspec_parallel Normal file
View File

@@ -0,0 +1,3 @@
--format progress
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
--tag ~performance

View File

@@ -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

View File

@@ -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

View File

@@ -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, */*"

View File

@@ -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

View File

@@ -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 }

View File

@@ -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()

View File

@@ -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

View File

@@ -0,0 +1 @@
angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown'])

View File

@@ -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(",")

View File

@@ -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'
})

View File

@@ -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

View File

@@ -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 )
]

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) ->
$scope.expanded = false

View File

@@ -0,0 +1,5 @@
angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) ->
link: (scope, element, attrs) ->
element.click (event) ->
event.stopPropagation()
scope.$emit "offClick"

View File

@@ -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

View File

@@ -0,0 +1 @@
angular.module("admin.dropdown", [])

View File

@@ -0,0 +1,4 @@
angular.module("admin.indexUtils").controller "ColumnsCtrl", ($scope, Columns) ->
$scope.columns = Columns.columns
$scope.predicate = ""
$scope.reverse = false

View File

@@ -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 )

View File

@@ -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"

View File

@@ -0,0 +1 @@
angular.module("admin.indexUtils", ['ngResource']).config ($httpProvider) ->

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
]

View File

@@ -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}

View File

@@ -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
]

View File

@@ -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)
]

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@
*= require shared/jquery-ui-timepicker-addon
*= require shared/textAngular.min
*= require shared/ng-tags-input.min
*= require_self
*= require_tree .

View File

@@ -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;
}
}
}

View 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}

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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 &nbsp;
.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 &nbsp;
-# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true }
-# %span{ :class => 'icon-check' } &nbsp; 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 &nbsp;
.three.columns.omega
%div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' }
%span{ :class => 'icon-reorder' } &nbsp; 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 && "&#10003;" || !column.visible && "&nbsp;" }}
%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?&nbsp;
%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()' }

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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?&nbsp;
%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()' }

View File

@@ -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

View File

@@ -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' }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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 } }

View File

@@ -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 ]

View File

@@ -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"

View File

@@ -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", ->

View File

@@ -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) }

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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) }

View File

@@ -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

View File

@@ -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