Merge branch 'master' into ginerr_bugfixes

This commit is contained in:
Rohan Mitchell
2015-06-05 15:49:43 +10:00
219 changed files with 3616 additions and 1787 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", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey"
($scope, $http, 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 }
@@ -32,7 +30,8 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
variant: { name: "Variant", visible: true }
quantity: { name: "Quantity", visible: true }
max: { name: "Max", visible: true }
unit_value: { name: "Weight/Volume", visible: false }
price: { name: "Price", visible: false }
$scope.initialise = ->
$scope.initialiseVariables()
authorise_api_reponse = ""
@@ -42,13 +41,15 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
if $scope.spree_api_key_ok
$http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").then (data) ->
$scope.suppliers = data
$scope.suppliers = $filter('orderBy')(data, 'name')
$scope.suppliers.unshift blankOption()
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_distributor_eq]=true").then (data) ->
$scope.distributors = data
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[sells_in][]=own&q[sells_in][]=any").then (data) ->
$scope.distributors = $filter('orderBy')(data, 'name')
$scope.distributors.unshift blankOption()
ocFetcher = dataFetcher("/api/order_cycles/accessible").then (data) ->
ocFetcher = dataFetcher("/api/order_cycles/accessible?as=distributor&q[orders_close_at_gt]=#{formatDate(daysFromToday(-90))}").then (data) ->
$scope.orderCycles = data
$scope.orderCyclesByID = []
$scope.orderCyclesByID[oc.id] = oc for oc in $scope.orderCycles
$scope.orderCycles.unshift blankOption()
$scope.fetchOrders()
ocFetcher.then ->
@@ -60,7 +61,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
$scope.fetchOrders = ->
$scope.loading = true
dataFetcher("/api/orders/managed?template=bulk_index;page=1;per_page=500;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) ->
dataFetcher("/admin/orders/managed?template=bulk_index;page=1;per_page=500;q[state_not_eq]=canceled;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) ->
$scope.resetOrders data
$scope.loading = false
@@ -162,6 +163,25 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
$scope.supplierFilter = $scope.suppliers[0].id
$scope.orderCycleFilter = $scope.orderCycles[0].id
$scope.quickSearch = ""
$scope.weightAdjustedPrice = (lineItem, oldValue) ->
if oldValue <= 0
oldValue = lineItem.units_variant.unit_value
if lineItem.unit_value <= 0
lineItem.unit_value = lineItem.units_variant.unit_value
lineItem.price = lineItem.price * lineItem.unit_value / oldValue
#$scope.bulk_order_form.line_item.price.$setViewValue($scope.bulk_order_form.line_item.price.$viewValue)
$scope.unitValueLessThanZero = (lineItem) ->
if lineItem.units_variant.unit_value <= 0
true
else
false
$scope.$watch "orderCycleFilter", (newVal, oldVal) ->
unless $scope.orderCycleFilter == "0" || angular.equals(newVal, oldVal)
$scope.startDate = $scope.orderCyclesByID[$scope.orderCycleFilter].first_order
$scope.endDate = $scope.orderCyclesByID[$scope.orderCycleFilter].last_order
]
daysFromToday = (days) ->

View File

@@ -1,16 +1,18 @@
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth) ->
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}
unit: {name: "Unit", visible: true}
price: {name: "Price", visible: true}
on_hand: {name: "On Hand", visible: true}
on_demand: {name: "On Demand", visible: false}
category: {name: "Category", visible: false}
tax_category: {name: "Tax Category", visible: false}
inherits_properties: {name: "Inherits Properties?", visible: false}
available_on: {name: "Available On", visible: false}
@@ -32,6 +34,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.producers = producers
$scope.taxons = Taxons.taxons
$scope.tax_categories = tax_categories
$scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
$scope.producerFilter = "0"
@@ -106,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()
@@ -137,15 +146,18 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.deleteVariant = (product, variant) ->
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
if product.variants.length > 1
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
).success (data) ->
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
).success (data) ->
$scope.removeVariant(product, variant)
alert("The last variant cannot be deleted!")
$scope.removeVariant = (product, variant) ->
product.variants.splice product.variants.indexOf(variant), 1
@@ -309,9 +321,15 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_demand = product.on_demand
hasUpdatableProperty = true
if product.hasOwnProperty("category_id")
filteredProduct.primary_taxon_id = product.category_id
hasUpdatableProperty = true
if product.hasOwnProperty("tax_category_id")
filteredProduct.tax_category_id = product.tax_category_id
hasUpdatableProperty = true
if product.hasOwnProperty("inherits_properties")
filteredProduct.inherits_properties = product.inherits_properties
hasUpdatableProperty = true
@@ -337,6 +355,9 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("on_hand")
filteredVariant.on_hand = variant.on_hand
hasUpdatableProperty = true
if variant.hasOwnProperty("on_demand")
filteredVariant.on_demand = variant.on_demand
hasUpdatableProperty = true
if variant.hasOwnProperty("price")
filteredVariant.price = variant.price
hasUpdatableProperty = true

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,23 +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
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

@@ -5,6 +5,13 @@ angular.module("admin.products")
$scope.placeholder_text = ""
$scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description]', ->
$scope.processVariantUnitWithScale()
$scope.processUnitValueWithDescription()
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
$scope.processVariantUnitWithScale = ->
if $scope.product.variant_unit_with_scale
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
@@ -16,18 +23,15 @@ angular.module("admin.products")
else
$scope.product.variant_unit = $scope.product.variant_unit_scale = null
$scope.processUnitValueWithDescription = ->
if $scope.product.master.hasOwnProperty("unit_value_with_description")
match = $scope.product.master.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
match = $scope.product.master.unit_value_with_description.match(/^([\d\.]+(?= *|$)|)( *)(.*)$/)
if match
$scope.product.master.unit_value = parseFloat(match[1])
$scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value)
$scope.product.master.unit_value *= $scope.product.variant_unit_scale if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_description = match[3]
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
$scope.hasVariants = (product) ->
Object.keys(product.variants).length > 0

View File

@@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher)
# when a respond_overrride for the clone action is used.
id = data.product.id
dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) =>
@addProducts [newProduct]
@insertProductAfter(product, newProduct)
updateVariantLists: (serverProducts, productsWithUnsavedVariants) ->
for product in productsWithUnsavedVariants
@@ -39,6 +39,10 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher)
@unpackProduct product
@products.push product
insertProductAfter: (product, newProduct) ->
index = @products.indexOf(product)
@products.splice(index + 1, 0, newProduct)
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product

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

View File

@@ -1,9 +1,10 @@
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService) ->
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService, EnterpriseModal) ->
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.query = Search.search()
$scope.openModal = EnterpriseModal.open
$scope.activeTaxons = []
$scope.show_profiles = false
$scope.filtersActive = false

View File

@@ -1,8 +1,3 @@
Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) ->
$scope.Groups = Groups
$scope.order = 'position'
#$rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) ->
#$anchorScroll()
#
#

View File

@@ -32,8 +32,9 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro
if product.variants
product.variants = (Variants.register variant for variant in product.variants)
variant.product = product for variant in product.variants
product.master.product = product
product.master = Variants.register product.master if product.master
if product.master
product.master.product = product
product.master = Variants.register product.master
registerVariantsWithCart: ->
for product in @products

File diff suppressed because one or more lines are too long

View File

@@ -14,7 +14,7 @@
.small-12.columns
.alert-box.info{ "ofn-inline-alert" => true, ng: { show: "visible" } }
%h6 Success! {{ enterprise.name }} added to the Open Food Network
%span If you exit the wizard at any stage, login and go to admin to edit or update your enterprise details.
%span If you exit this wizard at any stage, you need to click the confirmation link in the email you have received. This will take you to your admin interface where you can continue setting up your profile.
%a.close{ ng: { click: "close()" } } &times;
.small-12.large-8.columns

View File

@@ -18,7 +18,7 @@
%p
We've sent a confirmation email to
%strong {{ enterprise.email }}.
%strong {{ enterprise.email }} if it hasn't been activated before.
%br Please follow the instructions there to make your enterprise visible on the Open Food Network.
%a.button.primary{ type: "button", href: "/" } Open Food Network home &gt;

View File

@@ -5,7 +5,7 @@
%h4
%small
%i.ofn-i_040-hub
Create your enterprise profile
You can now create a profile for your Producer or Hub
.hide-for-large-up
%hr
%input.button.small.primary{ type: "button", value: "Let's get started!", ng: { click: "select('details')" } }
@@ -38,6 +38,7 @@
%strong contact
you on the Open Food Network.
%p Use this space to tell the story of your enterprise, to help drive connections to your social and online presence.
%p It's also the first step towards trading on the Open Food Network, or opening an online store.
.row.show-for-large-up
.small-12.columns

View File

@@ -38,9 +38,13 @@
%i.ofn-i_013-help
&nbsp;
%p Producers make yummy things to eat &amp;/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it.
/ %p Hubs connect the producer to the eater. Hubs can be co-ops, independent retailers, buying groups, wholesalers, CSA box schemes, farm-gate stalls, etc.
.panel.callout
.left
%i.ofn-i_013-help
&nbsp;
%p If youre not a producer, youre probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other.
.row.buttons
.small-12.columns
%input.button.secondary{ type: "button", value: "Back", ng: { click: "select('contact')" } }
%input.button.primary.right{ type: "submit", value: "Continue" }
%input.button.primary.right{ type: "submit", value: "Create Profile" }

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

@@ -249,3 +249,18 @@ span.required {
color: red;
font-size: 110%;
}
table td.actions {
.icon-trash, .icon-edit, icon-copy {
&.disabled {
border-color: #d0d0d0;
color: #c0c0c0;
background-color: #fafafa;
&:hover {
border-color: #a5a5a5;
color: #a5a5a5;
background-color: #fafafa;
}
}
}
}

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

@@ -74,6 +74,24 @@
.active_table_row:nth-child(2)
padding-bottom: 0.75rem
.producers-list
li.more-producers-link
.less
display: none
a:hover
text-decoration: underline
li.additional-producer
display: none
&.show-more-producers
li.additional-producer
display: block
li.more-producers-link
.more
display: none
.less
display: block
//CURRENT hub (shows selected hub)
&.current
//overwrites active_table
@@ -92,6 +110,7 @@
.active_table_row:first-child .skinny-head
background-color: rgba(255,255,255,0.85)
//INACTIVE - closed hub
&.inactive
&.closed, &.open

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

@@ -9,7 +9,7 @@ module Admin
def move_up
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group = EnterpriseGroup.find_by_permalink params[:enterprise_group_id]
@enterprise_group.move_higher
end
redirect_to main_app.admin_enterprise_groups_path
@@ -17,7 +17,7 @@ module Admin
def move_down
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group = EnterpriseGroup.find_by_permalink params[:enterprise_group_id]
@enterprise_group.move_lower
end
redirect_to main_app.admin_enterprise_groups_path
@@ -33,6 +33,12 @@ module Admin
end
alias_method_chain :build_resource, :address
# Overriding method on Spree's resource controller,
# so that resources are found using permalink
def find_resource
EnterpriseGroup.find_by_permalink(params[:id])
end
private
def load_data

View File

@@ -3,6 +3,7 @@ module Admin
before_filter :load_enterprise_set, :only => :index
before_filter :load_countries, :except => [:index, :set_sells, :check_permalink]
before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create]
before_filter :load_groups, :only => [:new, :edit, :update, :create]
before_filter :load_taxons, :only => [:new, :edit, :update, :create]
before_filter :check_can_change_sells, only: :update
before_filter :check_can_change_bulk_sells, only: :bulk_update
@@ -127,6 +128,10 @@ module Admin
@enterprise_fees = EnterpriseFee.managed_by(spree_current_user).for_enterprise(@enterprise).order(:fee_type, :name).all
end
def load_groups
@groups = EnterpriseGroup.managed_by(spree_current_user) | @enterprise.groups
end
def load_taxons
@taxons = Spree::Taxon.order(:name)
end

View File

@@ -13,7 +13,8 @@ module Api
end
def accessible
@enterprises = Enterprise.ransack(params[:q]).result.accessible_by(current_api_user)
permitted = OpenFoodNetwork::Permissions.new(current_api_user).order_cycle_enterprises
@enterprises = permitted.ransack(params[:q]).result
render params[:template] || :bulk_index
end

View File

@@ -9,7 +9,16 @@ module Api
end
def accessible
@order_cycles = OrderCycle.ransack(params[:q]).result.accessible_by(current_api_user)
@order_cycles = if params[:as] == "distributor"
OrderCycle.ransack(params[:q]).result.
involving_managed_distributors_of(current_api_user).order('updated_at DESC')
elsif params[:as] == "producer"
OrderCycle.ransack(params[:q]).result.
involving_managed_producers_of(current_api_user).order('updated_at DESC')
else
OrderCycle.ransack(params[:q]).result.accessible_by(current_api_user)
end
render params[:template] || :bulk_index
end
end

View File

@@ -12,9 +12,6 @@ class BaseController < ApplicationController
before_filter :check_order_cycle_expiry
def load_active_distributors
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
end
private

View File

@@ -12,9 +12,6 @@ class CheckoutController < Spree::CheckoutController
include EnterprisesHelper
def edit
# Because this controller doesn't inherit from our BaseController
# We need to duplicate the code here
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
end
def update

View File

@@ -4,7 +4,7 @@ class EnterprisesController < BaseController
include OrderCyclesHelper
# These prepended filters are in the reverse order of execution
prepend_before_filter :load_active_distributors, :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop
prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop
before_filter :clean_permalink, only: :check_permalink
respond_to :js, only: :permalink_checker

View File

@@ -1,12 +1,11 @@
class GroupsController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
@groups = EnterpriseGroup.on_front_page.by_position
end
def show
@group = EnterpriseGroup.find params[:id]
@group = EnterpriseGroup.find_by_permalink(params[:id]) || EnterpriseGroup.find(params[:id])
end
end

View File

@@ -1,11 +1,9 @@
class HomeController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end
def about_us
end
end

View File

@@ -1,6 +1,5 @@
class MapController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end

View File

@@ -1,7 +1,6 @@
class ProducersController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end
end

View File

@@ -10,12 +10,16 @@ class ShopController < BaseController
end
def products
# Can we make this query less slow?
#
if @products = products_for_shop
render status: 200,
json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer,
current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json
json: ActiveModel::ArraySerializer.new(@products,
each_serializer: Api::ProductSerializer,
current_order_cycle: current_order_cycle,
current_distributor: current_distributor,
variants: variants_for_shop_by_id,
master_variants: master_variants_for_shop_by_id).to_json
else
render json: "", status: 404
end
@@ -56,4 +60,30 @@ class ShopController < BaseController
"name ASC"
end
end
def all_variants_for_shop
# We use the in_stock? method here instead of the in_stock scope because we need to
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
# by them.
Spree::Variant.
for_distribution(current_order_cycle, current_distributor).
each { |v| v.scope_to_hub current_distributor }.
select(&:in_stock?)
end
def variants_for_shop_by_id
index_by_product_id all_variants_for_shop.reject(&:is_master)
end
def master_variants_for_shop_by_id
index_by_product_id all_variants_for_shop.select(&:is_master)
end
def index_by_product_id(variants)
variants.inject({}) do |vs, v|
vs[v.product_id] ||= []
vs[v.product_id] << v
vs
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

@@ -7,7 +7,7 @@ Spree::Admin::OrdersController.class_eval do
# We need to add expections for collection actions other than :index here
# because spree_auth_devise causes load_order to be called, which results
# in an auth failure as the @order object is nil for collection actions
before_filter :check_authorization, :except => :bulk_management
before_filter :check_authorization, except: [:bulk_management, :managed]
# After updating an order, the fees should be updated as well
# Currently, adding or deleting line items does not trigger updating the
@@ -17,12 +17,19 @@ Spree::Admin::OrdersController.class_eval do
after_filter :update_distribution_charge, :only => :update
respond_override :index => { :html =>
{ :success => lambda {
{ :success => lambda {
# Filter orders to only show those distributed by current user (or all for admin user)
@orders = @search.result.includes([:user, :shipments, :payments]).
distributed_by_user(spree_current_user).
page(params[:page]).
per(params[:per_page] || Spree::Config[:orders_per_page])
# Filter orders by distributor
if params[:distributor_ids]
@orders = @orders.where(distributor_id: params[:distributor_ids])
end
if params[:order_cycle_ids]
@orders = @orders.where(order_cycle_id: params[:order_cycle_ids])
end
} } }
# Overwrite to use confirm_email_for_customer instead of confirm_email.
@@ -37,4 +44,10 @@ Spree::Admin::OrdersController.class_eval do
def update_distribution_charge
@order.update_distribution_charge!
end
def managed
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
@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

@@ -406,27 +406,34 @@ Spree::Admin::ReportsController.class_eval do
end
params[:q][:meta_sort] ||= "completed_at.desc"
# -- Search
@search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q])
orders = @search.result
@line_items = orders.map do |o|
lis = o.line_items.managed_by(spree_current_user)
lis = lis.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present?
lis
end.flatten
#payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
# -- Prepare form options
my_distributors = Enterprise.is_distributor.managed_by(spree_current_user)
my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user)
# -- Search
@search = Spree::Order.complete.not_state(:canceled).search(params[:q])
orders = permissions.visible_orders.merge(@search.result)
@line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders))
@line_items = @line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present?
line_items_with_hidden_details = @line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items)
@line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item|
# TODO We should really be hiding customer code here too, but until we
# have an actual association between order and customer, it's a bit tricky
line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
line_item.order.assign_attributes(email: "HIDDEN")
end
# My distributors and any distributors distributing products I supply
@distributors = my_distributors | Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers))
@distributors = permissions.visible_enterprises_for_order_reports.is_distributor
# My suppliers and any suppliers supplying products I distribute
@suppliers = my_suppliers | my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq
@suppliers = permissions.visible_enterprises_for_order_reports.is_primary_producer
@order_cycles = OrderCycle.active_or_complete.
involving_managed_distributors_of(spree_current_user).order('orders_close_at DESC')
@order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC')
@report_types = REPORT_TYPES[:orders_and_fulfillment]
@report_type = params[:report_type]

View File

@@ -4,7 +4,7 @@ Spree::Admin::VariantsController.class_eval do
def search
search_params = { :product_name_cont => params[:q], :sku_cont => params[:q] }
@variants = Spree::Variant.ransack(search_params.merge(:m => 'or')).result
@variants = Spree::Variant.where(is_master: false).ransack(search_params.merge(:m => 'or')).result
if params[:order_cycle_id].present?
order_cycle = OrderCycle.find params[:order_cycle_id]

View File

@@ -0,0 +1,8 @@
Spree::Api::LineItemsController.class_eval do
after_filter :apply_enterprise_fees, :only => :update
def apply_enterprise_fees
authorize! :read, order
order.update_distribution_charge!
end
end

View File

@@ -4,12 +4,4 @@ Spree::Api::OrdersController.class_eval do
# because Spree's API controller causes authorize_read! to be called, which
# results in an ActiveRecord::NotFound Exception as the order object is not
# defined for collection actions
before_filter :authorize_read!, :except => [:managed]
def managed
authorize! :admin, Spree::Order
authorize! :read, Spree::Order
@orders = Spree::Order.ransack(params[:q]).result.distributed_by_user(current_api_user).page(params[:page]).per(params[:per_page])
respond_with(@orders, default_template: :index)
end
end

View File

@@ -11,7 +11,7 @@ Spree::Api::ProductsController.class_eval do
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
@products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products.
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
merge(product_scope).
order('created_at DESC').
ransack(params[:q]).result.

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
@@ -50,6 +54,10 @@ module Admin
admin_inject_json_ams_array "ofn.admin", "products", @products, Api::Admin::ProductSerializer
end
def admin_inject_tax_categories
admin_inject_json_ams_array "ofn.admin", "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer
end
def admin_inject_taxons
admin_inject_json_ams_array "admin.taxons", "taxons", @taxons, Api::Admin::TaxonSerializer
end

View File

@@ -1,6 +1,16 @@
require 'open_food_network/enterprise_injection_data'
module InjectionHelper
def inject_enterprises
inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors
inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_group_enterprises
inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_current_hub
inject_json_ams "currentHub", current_distributor, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_current_order
@@ -53,4 +63,13 @@ module InjectionHelper
end
render partial: "json/injection_ams", locals: {name: name, json: json}
end
private
def enterprise_injection_data
@enterprise_injection_data ||= OpenFoodNetwork::EnterpriseInjectionData.new
{data: @enterprise_injection_data}
end
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
@@ -162,17 +163,6 @@ class Enterprise < ActiveRecord::Base
end
}
# Return enterprises that participate in order cycles that user coordinates, sends to or receives from
scope :accessible_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
else
with_order_cycles_outer.
where('order_cycles.id IN (?)', OrderCycle.accessible_by(user)).
select('DISTINCT enterprises.*')
end
}
def self.find_near(suburb)
enterprises = []
@@ -189,6 +179,10 @@ class Enterprise < ActiveRecord::Base
count(distinct: true)
end
def activated?
confirmed_at.present? && sells != 'unspecified'
end
def set_producer_property(property_name, property_value)
transaction do
property = Spree::Property.where(name: property_name).first_or_create!(presentation: property_name)
@@ -223,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
@@ -310,7 +308,7 @@ class Enterprise < ActiveRecord::Base
test_permalink = test_permalink.parameterize
test_permalink = "my-enterprise" if test_permalink.blank?
existing = Enterprise.select(:permalink).order(:permalink).where("permalink LIKE ?", "#{test_permalink}%").map(&:permalink)
if existing.empty?
unless existing.include?(test_permalink)
test_permalink
else
used_indices = existing.map do |p|

View File

@@ -16,8 +16,12 @@ class EnterpriseGroup < ActiveRecord::Base
validates :name, presence: true
validates :description, presence: true
before_validation :sanitize_permalink
validates :permalink, uniqueness: true, presence: true
attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids
attr_accessible :owner_id
attr_accessible :permalink
attr_accessible :logo, :promo_image
attr_accessible :address_attributes
attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter
@@ -71,4 +75,31 @@ class EnterpriseGroup < ActiveRecord::Base
address.zipcode.sub!(/^undefined$/, '')
end
def to_param
permalink
end
private
def self.find_available_value(existing, requested)
return requested unless existing.include?(requested)
used_indices = existing.map do |p|
p.slice!(/^#{requested}/)
p.match(/^\d+$/).to_s.to_i
end
options = (1..used_indices.length + 1).to_a - used_indices
requested + options.first.to_s
end
def find_available_permalink(requested)
existing = self.class.where(id: !id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink)
self.class.find_available_value(existing, requested)
end
def sanitize_permalink
if permalink.blank? || permalink_changed?
requested = permalink.presence || permalink_was.presence || name.presence || 'group'
self.permalink = find_available_permalink(requested.parameterize)
end
end
end

View File

@@ -25,6 +25,32 @@ class EnterpriseRelationship < ActiveRecord::Base
scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name')
# Load an array of the relatives of each enterprise (ie. any enterprise related to it in
# either direction). This array is split into distributors and producers, and has the format:
# {enterprise_id => {distributors: [id, ...], producers: [id, ...]} }
def self.relatives(activated_only=false)
relationships = EnterpriseRelationship.includes(:child, :parent)
relatives = {}
relationships.each do |r|
relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new}
relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new}
if !activated_only || r.child.activated?
relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer
relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor
end
if !activated_only || r.parent.activated?
relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer
relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor
end
end
relatives
end
def permissions_list=(perms)
perms.andand.each { |name| permissions.build name: name }
end

View File

@@ -26,7 +26,7 @@ class OrderCycle < ActiveRecord::Base
closed.
where("order_cycles.orders_close_at >= ?", 31.days.ago).
order("order_cycles.orders_close_at DESC") }
scope :soonest_opening, lambda { upcoming.order('order_cycles.orders_open_at ASC') }
scope :distributing_product, lambda { |product|
@@ -64,6 +64,25 @@ class OrderCycle < ActiveRecord::Base
joins('LEFT OUTER JOIN enterprises ON (enterprises.id = exchanges.sender_id OR enterprises.id = exchanges.receiver_id)')
}
scope :involving_managed_distributors_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an outgoing exchange
# ie. coordinator or distibutor
joins(:exchanges).merge(Exchange.outgoing).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).
select('DISTINCT order_cycles.*')
}
scope :involving_managed_producers_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an incoming exchange
# ie. coordinator or producer
joins(:exchanges).merge(Exchange.incoming).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).
select('DISTINCT order_cycles.*')
}
def self.first_opening_for(distributor)
with_distributor(distributor).soonest_opening.first
@@ -73,11 +92,25 @@ class OrderCycle < ActiveRecord::Base
with_distributor(distributor).soonest_closing.first
end
def self.most_recently_closed_for(distributor)
with_distributor(distributor).most_recently_closed.first
end
# Find the earliest closing times for each distributor in an active order cycle, and return
# them in the format {distributor_id => closing_time, ...}
def self.earliest_closing_times
Hash[
Exchange.
outgoing.
joins(:order_cycle).
merge(OrderCycle.active).
group('exchanges.receiver_id').
select('exchanges.receiver_id AS receiver_id, MIN(order_cycles.orders_close_at) AS earliest_close_at').
map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] }
]
end
def clone!
oc = self.dup
oc.name = "COPY OF #{oc.name}"

View File

@@ -120,7 +120,7 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit], Spree::Classification
# Reports page
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
end
def add_order_cycle_management_abilities(user)
@@ -142,7 +142,7 @@ class AbilityDecorator
# during the order creation process from the admin backend
order.distributor.nil? || user.enterprises.include?(order.distributor)
end
can [:admin, :bulk_management], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
can [:admin, :bulk_management, :managed], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
can [:admin, :create], Spree::LineItem
can [:destroy], Spree::LineItem do |item|
user.admin? || user.enterprises.include?(order.distributor) || user == order.order_cycle.manager
@@ -154,7 +154,6 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization
can [:destroy], Spree::Adjustment do |adjustment|
# Sharing code with destroying a line item. This should be unified and probably applied for other actions as well.
binding.pry
if user.admin?
true
elsif adjustment.adjustable.instance_of? Spree::Order
@@ -185,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

@@ -1,5 +1,6 @@
Spree::LineItem.class_eval do
attr_accessible :max_quantity
attr_accessible :max_quantity, :unit_value
attr_accessible :unit_value, :price, :as => :api
# -- Scopes
scope :managed_by, lambda { |user|

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
@@ -127,6 +130,7 @@ Spree::Order.class_eval do
else
current_item = Spree::LineItem.new(:quantity => quantity, max_quantity: max_quantity)
current_item.variant = variant
current_item.unit_value = variant.unit_value
if currency
current_item.currency = currency unless currency.nil?
current_item.price = variant.price_in(currency).amount
@@ -260,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

@@ -23,17 +23,18 @@ Spree::Product.class_eval do
attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value
attr_accessible :inherits_properties, :sku
validates_associated :master, message: "^Price and On Hand must be valid"
# validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant"
validates_presence_of :supplier
validates :primary_taxon, presence: { message: "^Product Category can't be blank" }
validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category"
validates_presence_of :variant_unit, if: :has_variants?
validates_presence_of :variant_unit
validates_presence_of :variant_unit_scale,
if: -> p { %w(weight volume).include? p.variant_unit }
validates_presence_of :variant_unit_name,
if: -> p { p.variant_unit == 'items' }
after_save :ensure_standard_variant
after_initialize :set_available_on_to_now, :if => :new_record?
after_save :update_units
after_touch :touch_distributors
@@ -108,6 +109,12 @@ Spree::Product.class_eval do
# -- Methods
# Called by Spree::Product::duplicate before saving.
def duplicate_extra(parent)
# Spree sets the SKU to "COPY OF #{parent sku}".
self.master.sku = ''
end
def properties_including_inherited
# Product properties override producer properties
ps = product_properties.all
@@ -209,4 +216,31 @@ Spree::Product.class_eval do
Spree::OptionType.where('name LIKE ?', 'unit_%%')
end
def ensure_standard_variant
if master.valid? && variants.empty?
variant = self.master.dup
variant.product = self
variant.is_master = false
self.variants << variant
end
end
# Override Spree's old save_master method and replace it with the most recent method from spree repository
# This fixes any problems arising from failing master saves, without the need for a validates_associated on
# master, while giving us more specific errors as to why saving failed
def save_master
begin
if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed? || master.default_price.new_record?)))
master.save!
end
# If the master cannot be saved, the Product object will get its errors
# and will be destroyed
rescue ActiveRecord::RecordInvalid
master.errors.each do |att, error|
self.errors.add(att, error)
end
raise
end
end
end

View File

@@ -14,7 +14,7 @@ class Spree::ProductSet < ModelSet
if e.nil?
@klass.new(attributes).save unless @reject_if.andand.call(attributes)
else
e.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) and
( attributes.except(:id, :variants_attributes, :master_attributes).present? ? e.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) : true) and
(attributes[:variants_attributes] ? update_variants_attributes(e, attributes[:variants_attributes]) : true ) and
(attributes[:master_attributes] ? update_variant(e, attributes[:master_attributes]) : true )
end

View File

@@ -25,6 +25,22 @@ Spree::ShippingMethod.class_eval do
scope :by_name, order('spree_shipping_methods.name ASC')
# Return the services (pickup, delivery) that different distributors provide, in the format:
# {distributor_id => {pickup: true, delivery: false}, ...}
def self.services
Hash[
Spree::ShippingMethod.
joins(:distributor_shipping_methods).
group('distributor_id').
select("distributor_id").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery").
map { |sm| [sm.distributor_id.to_i, {pickup: sm.pickup == 't', delivery: sm.delivery == 't'}] }
]
end
def available_to_order_with_distributor_check?(order, display_on=nil)
available_to_order_without_distributor_check?(order, display_on) &&
self.distributors.include?(order.distributor)

View File

@@ -9,4 +9,40 @@ Spree::Taxon.class_eval do
#fs << Spree::ProductFilters.distributor_filter if Spree::ProductFilters.respond_to? :distributor_filter
fs
end
# Find all the taxons of supplied products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.supplied_taxons
taxons = {}
Spree::Taxon.
joins(:products => :supplier).
select('spree_taxons.*, enterprises.id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
taxons[t.enterprise_id.to_i] << t.id
end
taxons
end
# Find all the taxons of distributed products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.distributed_taxons
taxons = {}
Spree::Taxon.
joins(:products).
merge(Spree::Product.with_order_cycles_outer).
where('o_exchanges.incoming = ?', false).
select('spree_taxons.*, o_exchanges.receiver_id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
taxons[t.enterprise_id.to_i] << t.id
end
taxons
end
end

View File

@@ -1,5 +1,9 @@
Spree.user_class.class_eval do
handle_asynchronously :send_reset_password_instructions
if method_defined? :send_reset_password_instructions_with_delay
Bugsnag.notify RuntimeError.new "send_reset_password_instructions already handled asyncronously - double-calling results in infinite job loop"
else
handle_asynchronously :send_reset_password_instructions
end
has_many :enterprise_roles, :dependent => :destroy
has_many :enterprises, through: :enterprise_roles

View File

@@ -7,17 +7,16 @@ Spree::Variant.class_eval do
has_many :exchange_variants, dependent: :destroy
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides
attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name
accepts_nested_attributes_for :images
validates_presence_of :unit_value,
if: -> v { %w(weight volume).include? v.product.andand.variant_unit },
unless: :is_master
if: -> v { %w(weight volume).include? v.product.andand.variant_unit }
validates_presence_of :unit_description,
if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? },
unless: :is_master
if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }
before_validation :update_weight_from_unit_value, if: -> v { v.product.present? }
after_save :update_units
@@ -110,9 +109,15 @@ Spree::Variant.class_eval do
end
def delete
transaction do
self.update_column(:deleted_at, Time.now)
ExchangeVariant.where(variant_id: self).destroy_all
if product.variants == [self] # Only variant left on product
errors.add :product, "must have at least one variant"
false
else
transaction do
self.update_column(:deleted_at, Time.now)
ExchangeVariant.where(variant_id: self).destroy_all
self
end
end
end

View File

@@ -0,0 +1,13 @@
/ insert_before "div.clearfix"
.field-block.alpha.eight.columns
= label_tag nil, t(:distributors)
= select_tag(:distributor_ids,
options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]),
{class: "select2 fullwidth", multiple: true})
.field-block.alpha.eight.columns
= label_tag nil, t(:order_cycles)
= select_tag(:order_cycle_ids,
options_for_select(OrderCycle.managed_by(spree_current_user).map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]),
{class: "select2 fullwidth", multiple: true})

View File

@@ -38,20 +38,26 @@
.twelve.columns.alpha
.six.columns.alpha
= render 'spree/admin/products/primary_taxon_form', f: f
.three.columns
.two.columns
= f.field_container :price do
= f.label :price, t(:price)
%span.required *
%br/
= f.text_field :price, class: 'fullwidth'
= f.error_message_on :price
.three.columns.omega
.two.columns
= f.field_container :on_hand do
= f.label :on_hand, t(:on_hand)
%span.required *
%br/
= f.text_field :on_hand, class: 'fullwidth'
= f.error_message_on :on_hand
.two.columns.omega
= f.field_container :on_demand do
= f.label :on_demand, t(:on_demand)
%br/
= f.check_box :on_demand
= f.error_message_on :on_demand
.twelve.columns.alpha
.six.columns.alpha &nbsp;
.three.columns

View File

@@ -0,0 +1,14 @@
class Api::Admin::BasicOrderCycleSerializer < ActiveModel::Serializer
attributes :id, :name, :first_order, :last_order
has_many :suppliers, serializer: Api::Admin::IdNameSerializer
has_many :distributors, serializer: Api::Admin::IdNameSerializer
def first_order
object.orders_open_at.strftime("%F")
end
def last_order
(object.orders_close_at + 1.day).strftime("%F")
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,19 @@
class Api::Admin::LineItemSerializer < ActiveModel::Serializer
attributes :id, :quantity, :max_quantity, :supplier, :price, :unit_value, :units_product, :units_variant
def supplier
Api::Admin::IdNameSerializer.new(object.product.supplier).serializable_hash
end
def units_product
Api::Admin::UnitsProductSerializer.new(object.product).serializable_hash
end
def units_variant
Api::Admin::UnitsVariantSerializer.new(object.variant).serializable_hash
end
def unit_value
object.unit_value.to_f
end
end

View File

@@ -0,0 +1,31 @@
class Api::Admin::OrderSerializer < ActiveModel::Serializer
attributes :id, :number, :full_name, :email, :phone, :completed_at, :line_items
has_one :distributor, serializer: Api::Admin::IdNameSerializer
has_one :order_cycle, serializer: Api::Admin::BasicOrderCycleSerializer
def full_name
object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" )
end
def email
object.email || ""
end
def phone
object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" )
end
def completed_at
object.completed_at.blank? ? "" : object.completed_at.strftime("%F %T")
end
def line_items
# we used to have a scope here, but we are at the point where a user which can edit an order
# should be able to edit all of the line_items as well, making the scope redundant
ActiveModel::ArraySerializer.new(
object.line_items.order('id ASC'),
{each_serializer: Api::Admin::LineItemSerializer}
)
end
end

View File

@@ -1,7 +1,7 @@
class Api::Admin::ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
attributes :on_hand, :price, :available_on, :permalink_live
attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id
has_one :supplier, key: :producer_id, embed: :id
has_one :primary_taxon, key: :category_id, embed: :id

View File

@@ -0,0 +1,3 @@
class Api::Admin::TaxCategorySerializer < ActiveModel::Serializer
attributes :id, :name
end

View File

@@ -0,0 +1,3 @@
class Api::Admin::UnitsProductSerializer < ActiveModel::Serializer
attributes :id, :name, :group_buy_unit_size, :variant_unit
end

View File

@@ -0,0 +1,8 @@
class Api::Admin::UnitsVariantSerializer < ActiveModel::Serializer
attributes :id, :full_name, :unit_value
def full_name
full_name = object.full_name
object.product.name + (full_name.empty? ? "" : ": #{full_name}")
end
end

View File

@@ -1,6 +1,7 @@
class Api::Admin::VariantSerializer < ActiveModel::Serializer
attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display
attributes :on_hand, :price
has_many :variant_overrides
def on_hand
object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" )

View File

@@ -1,4 +1,7 @@
class Api::EnterpriseSerializer < ActiveModel::Serializer
# We reference this here because otherwise the serializer complains about its absence
Api::IdSerializer
def serializable_hash
cached_serializer_hash.merge uncached_serializer_hash
end
@@ -6,11 +9,11 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer
private
def cached_serializer_hash
Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash
Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash || {}
end
def uncached_serializer_hash
Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash
Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash || {}
end
end
@@ -18,19 +21,22 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer
attributes :orders_close_at, :active
def orders_close_at
OrderCycle.first_closing_for(object).andand.orders_close_at
options[:data].earliest_closing_times[object.id]
end
def active
@options[:active_distributors].andand.include? object
options[:data].active_distributors.andand.include? object
end
end
class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
cached
delegate :cache_key, to: :object
#delegate :cache_key, to: :object
def cache_key
object.andand.cache_key
end
attributes :name, :id, :description, :latitude, :longitude,
:long_description, :website, :instagram, :linkedin, :twitter,
@@ -38,17 +44,27 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
:email, :hash, :logo, :promo_image, :path, :pickup, :delivery,
:icon, :icon_font, :producer_icon_font, :category, :producers, :hubs
has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer
has_many :supplied_taxons, serializer: Api::IdSerializer
attributes :taxons, :supplied_taxons
has_one :address, serializer: Api::AddressSerializer
def taxons
ids_to_objs options[:data].distributed_taxons[object.id]
end
def supplied_taxons
ids_to_objs options[:data].supplied_taxons[object.id]
end
def pickup
object.shipping_methods.where(:require_ship_address => false).present?
services = options[:data].shipping_method_services[object.id]
services ? services[:pickup] : false
end
def delivery
object.shipping_methods.where(:require_ship_address => true).present?
services = options[:data].shipping_method_services[object.id]
services ? services[:delivery] : false
end
def email
@@ -72,11 +88,13 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
end
def producers
ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer})
relatives = options[:data].relatives[object.id]
relatives ? ids_to_objs(relatives[:producers]) : []
end
def hubs
ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer})
relatives = options[:data].relatives[object.id]
relatives ? ids_to_objs(relatives[:distributors]) : []
end
# Map svg icons.
@@ -116,4 +134,11 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
}
icon_fonts[object.category]
end
private
def ids_to_objs(ids)
ids.andand.map { |id| {id: id} }
end
end

View File

@@ -30,8 +30,9 @@ class Api::CachedProductSerializer < ActiveModel::Serializer
#cached
#delegate :cache_key, to: :object
attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy,
:notes, :description, :properties_with_values
attributes :id, :name, :permalink, :count_on_hand
attributes :on_demand, :group_buy, :notes, :description
attributes :properties_with_values
has_many :variants, serializer: Api::VariantSerializer
has_many :taxons, serializer: Api::IdSerializer
@@ -46,13 +47,11 @@ class Api::CachedProductSerializer < ActiveModel::Serializer
end
def variants
# We use the in_stock? method here instead of the in_stock scope because we need to
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
# by them.
object.variants.
for_distribution(options[:current_order_cycle], options[:current_distributor]).
each { |v| v.scope_to_hub options[:current_distributor] }.
select(&:in_stock?)
options[:variants][object.id] || []
end
def master
options[:master_variants][object.id].andand.first
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

@@ -19,3 +19,8 @@
= f.label :enterprise_ids, 'Enterprises'
%br/
= f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true}
= f.field_container :permalink do
= f.label :permalink, "Permalink (unique, no spaces)"
%br/
= f.text_field :permalink

View File

@@ -5,15 +5,15 @@
%span.required *
.eight.columns.omega
= f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" }
.row
.alpha.eleven.columns
.three.columns.alpha
= f.label :group_ids, 'Groups'
.with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."}
%a What's this?
.eight.columns.omega
= f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..."
- if @groups.present?
.row
.alpha.eleven.columns
.three.columns.alpha
= f.label :group_ids, 'Groups'
.with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."}
%a What's this?
.eight.columns.omega
= f.collection_select :group_ids, @groups, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..."
.row
.three.columns.alpha

View File

@@ -1,9 +1,11 @@
object @order_cycle
attributes :id, :name
node( :first_order ) { |order| order.orders_open_at.strftime("%F") }
node( :last_order ) { |order| (order.orders_close_at + 1.day).strftime("%F") }
node( :suppliers ) do |oc|
partial 'api/enterprises/bulk_index', :object => oc.suppliers
end
node( :distributors ) do |oc|
partial 'api/enterprises/bulk_index', :object => oc.distributors
end
end

View File

@@ -1,20 +1,22 @@
%h3
= "Hi, #{@resource.contact}!"
%p.lead
= "Please confirm your email address for "
%strong
= "#{@resource.name}."
= "A profile for #{@resource.name} has been successfully created!"
To activate your Profile we need to confirm this email address.
%p &nbsp;
%p.callout
Click the link below to confirm your email and to activate your enterprise. This link can be used only once:
Please click the link below to confirm your email and to continue setting up your profile.
%br
%strong
= link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token)
%p &nbsp;
%p
= "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions."
After confirming your email you can access your administration account for this enterprise.
See the
= link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/'
= "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store."
= render 'shared/mailers/signoff'

View File

@@ -1,67 +1,27 @@
%h3
= "Welcome, #{@enterprise.contact}!"
%p.lead
Congratulations,
Thank you for confirming your email address.
%strong
%strong= @enterprise.name
= @enterprise.name
= "is now part of #{ Spree::Config.site_name }!"
/ Heading Panel
%p
Please find below all the details for viewing and editing your enterprise on
%strong= "#{ Spree::Config.site_name }."
We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services.
The User Guide with detailed support for setting up your Producer or Hub is here:
= link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/'
-#%p &nbsp;
-# %p.callout
-# %strong
-# Your enterprise details
-# %table{:width => "100%"}
-# %tr
-# %td{:align => "right"}
-# %strong
-# Shop URL
-# %td &nbsp;
-# %td
-# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"}
-# = main_app.enterprise_shop_url(@enterprise)
-# %tr
-# %td &nbsp;
-# %tr
-# %td{:align => "right"}
-# %strong
-# Email
-# %td &nbsp;
-# %td
-# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"}
-# = @enterprise.email
%p &nbsp;
%p
Log into
%strong= "#{ Spree::Config.site_name } Admin"
in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise!
You can manage your account by logging into the
= link_to 'Admin Panel', spree.admin_url
or by clicking on the cog in the top right hand side of the homepage, and selecting Administration.
%p.callout
%strong
OFN Admin
%table{ :width => "100%"}
%tr
%td{:align => "right"}
%strong
Admin
%td &nbsp;
%td
%a{:href => "#{ spree.admin_url }", :target => "_blank"}
= spree.admin_url
%p &nbsp;
/ /Heading Panel
%p
We're so pleased to have you as a valued member of
%strong= "#{Spree::Config.site_name}!"
Don't hestitate to get in touch if you have any questions.
We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next.
= link_to 'Join the community.', 'http://community.openfoodnetwork.org/'
%p
If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out!
= render 'shared/mailers/signoff'
= render 'shared/mailers/social_and_contact'
= render 'shared/mailers/social_and_contact'

Some files were not shown because too many files have changed in this diff Show More