mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-30 06:31:16 +00:00
Merge branch 'master' into ginerr_bugfixes
This commit is contained in:
3
.rspec_parallel
Normal file
3
.rspec_parallel
Normal file
@@ -0,0 +1,3 @@
|
||||
--format progress
|
||||
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
|
||||
--tag ~performance
|
||||
2
Gemfile
2
Gemfile
@@ -49,6 +49,7 @@ gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg'
|
||||
gem 'angularjs-file-upload-rails', '~> 1.1.0'
|
||||
gem 'roadie-rails', '~> 1.0.3'
|
||||
gem 'figaro'
|
||||
gem 'acts-as-taggable-on', '~> 3.4'
|
||||
|
||||
gem 'foreigner'
|
||||
gem 'immigrant'
|
||||
@@ -112,4 +113,5 @@ group :development do
|
||||
gem 'guard-rails'
|
||||
gem 'guard-zeus'
|
||||
gem 'guard-rspec'
|
||||
gem 'parallel_tests'
|
||||
end
|
||||
|
||||
@@ -142,6 +142,8 @@ GEM
|
||||
activesupport (3.2.21)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
acts-as-taggable-on (3.5.0)
|
||||
activerecord (>= 3.2, < 5)
|
||||
acts_as_list (0.1.4)
|
||||
addressable (2.3.3)
|
||||
andand (1.3.3)
|
||||
@@ -362,6 +364,9 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
cocaine (~> 0.5.3)
|
||||
mime-types
|
||||
parallel (1.4.1)
|
||||
parallel_tests (1.3.7)
|
||||
parallel
|
||||
paypal-sdk-core (0.2.10)
|
||||
multi_json (~> 1.0)
|
||||
xml-simple
|
||||
@@ -532,6 +537,7 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
active_model_serializers
|
||||
acts-as-taggable-on (~> 3.4)
|
||||
andand
|
||||
angular-rails-templates
|
||||
angularjs-file-upload-rails (~> 1.1.0)
|
||||
@@ -575,6 +581,7 @@ DEPENDENCIES
|
||||
newrelic_rpm
|
||||
oj
|
||||
paperclip
|
||||
parallel_tests
|
||||
pg
|
||||
poltergeist
|
||||
pry-debugger
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) ->
|
||||
angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) ->
|
||||
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content")
|
||||
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
//= require admin/spree_promo
|
||||
//= require admin/spree_paypal_express
|
||||
//= require ../shared/ng-infinite-scroll.min.js
|
||||
//= require ../shared/ng-tags-input.min.js
|
||||
//= require ./admin
|
||||
//= require ./customers/customers
|
||||
//= require ./dropdown/dropdown
|
||||
//= require ./enterprises/enterprises
|
||||
//= require ./enterprise_groups/enterprise_groups
|
||||
//= require ./index_utils/index_utils
|
||||
//= require ./payment_methods/payment_methods
|
||||
//= require ./products/products
|
||||
//= require ./shipping_methods/shipping_methods
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
|
||||
"$scope", "$http", "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) ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) ->
|
||||
$scope.shop = null
|
||||
$scope.shops = shops
|
||||
$scope.submitAll = pendingChanges.submitAll
|
||||
|
||||
$scope.columns = Columns.setColumns
|
||||
email: { name: "Email", visible: true }
|
||||
code: { name: "Code", visible: true }
|
||||
tags: { name: "Tags", visible: true }
|
||||
|
||||
$scope.$watch "shop", ->
|
||||
if $scope.shop?
|
||||
Customers.loaded = false
|
||||
$scope.customers = Customers.index(enterprise_id: $scope.shop.id)
|
||||
|
||||
$scope.loaded = ->
|
||||
Customers.loaded
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown'])
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.customers").directive "tagsWithTranslation", ->
|
||||
restrict: "E"
|
||||
template: "<tags-input ng-model='object.tags'>"
|
||||
scope:
|
||||
object: "="
|
||||
link: (scope, element, attrs) ->
|
||||
scope.$watchCollection "object.tags", ->
|
||||
scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",")
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.customers").factory 'CustomerResource', ($resource) ->
|
||||
$resource('/admin/customers.json', {}, {
|
||||
'index':
|
||||
method: 'GET'
|
||||
isArray: true
|
||||
params:
|
||||
enterprise_id: '@enterprise_id'
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
angular.module("admin.customers").factory 'Customers', (CustomerResource) ->
|
||||
new class Customers
|
||||
customers: []
|
||||
customers_by_id: {}
|
||||
loaded: false
|
||||
|
||||
index: (params={}, callback=null) ->
|
||||
CustomerResource.index params, (data) =>
|
||||
for customer in data
|
||||
@customers.push customer
|
||||
@customers_by_id[customer.id] = customer
|
||||
|
||||
@loaded = true
|
||||
(callback || angular.noop)(@customers)
|
||||
|
||||
@customers
|
||||
@@ -1,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 )
|
||||
]
|
||||
@@ -1,10 +1,8 @@
|
||||
angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) ->
|
||||
link: (scope, element, attrs) ->
|
||||
if DisplayProperties.showVariants scope.product.id
|
||||
element.removeClass "icon-chevron-right"
|
||||
element.addClass "icon-chevron-down"
|
||||
else
|
||||
element.removeClass "icon-chevron-down"
|
||||
element.addClass "icon-chevron-right"
|
||||
|
||||
element.on "click", ->
|
||||
@@ -16,4 +14,4 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -
|
||||
else
|
||||
DisplayProperties.setShowVariants scope.product.id, true
|
||||
element.removeClass "icon-chevron-right"
|
||||
element.addClass "icon-chevron-down"
|
||||
element.addClass "icon-chevron-down"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) ->
|
||||
$scope.expanded = false
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.click (event) ->
|
||||
event.stopPropagation()
|
||||
scope.$emit "offClick"
|
||||
@@ -1,6 +1,4 @@
|
||||
dropDownModule = angular.module("ofn.dropdown", [])
|
||||
|
||||
dropDownModule.directive "ofnDropDown", ($document) ->
|
||||
angular.module("admin.dropdown").directive "ofnDropDown", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
outsideClickListener = (event) ->
|
||||
unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") ||
|
||||
@@ -20,12 +18,3 @@ dropDownModule.directive "ofnDropDown", ($document) ->
|
||||
scope.$apply ->
|
||||
scope.expanded = true
|
||||
element.addClass "expanded"
|
||||
|
||||
dropDownModule.directive "ofnCloseOnClick", ($document) ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.click (event) ->
|
||||
event.stopPropagation()
|
||||
scope.$emit "offClick"
|
||||
|
||||
dropDownModule.controller "DropDownCtrl", ($scope) ->
|
||||
$scope.expanded = false
|
||||
1
app/assets/javascripts/admin/dropdown/dropdown.js.coffee
Normal file
1
app/assets/javascripts/admin/dropdown/dropdown.js.coffee
Normal file
@@ -0,0 +1 @@
|
||||
angular.module("admin.dropdown", [])
|
||||
@@ -0,0 +1,4 @@
|
||||
angular.module("admin.indexUtils").controller "ColumnsCtrl", ($scope, Columns) ->
|
||||
$scope.columns = Columns.columns
|
||||
$scope.predicate = ""
|
||||
$scope.reverse = false
|
||||
@@ -0,0 +1,36 @@
|
||||
angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendingChanges) ->
|
||||
scope:
|
||||
object: "&objForUpdate"
|
||||
type: "@objForUpdate"
|
||||
attr: "@attrForUpdate"
|
||||
link: (scope, element, attrs) ->
|
||||
scope.savedValue = scope.object()[scope.attr]
|
||||
|
||||
scope.$watch "object().#{scope.attr}", (value) ->
|
||||
if value == scope.savedValue
|
||||
pendingChanges.remove(scope.object().id, scope.attr)
|
||||
scope.clear()
|
||||
else
|
||||
change =
|
||||
object: scope.object()
|
||||
type: scope.type
|
||||
attr: scope.attr
|
||||
value: value
|
||||
scope: scope
|
||||
scope.pending()
|
||||
pendingChanges.add(scope.object().id, scope.attr, change)
|
||||
|
||||
scope.reset = (value) ->
|
||||
scope.savedValue = value
|
||||
|
||||
scope.success = ->
|
||||
switchClass( element, "update-success", ["update-pending", "update-error"], 3000 )
|
||||
|
||||
scope.pending = ->
|
||||
switchClass( element, "update-pending", ["update-error", "update-success"], false )
|
||||
|
||||
scope.error = ->
|
||||
switchClass( element, "update-error", ["update-pending", "update-success"], false )
|
||||
|
||||
scope.clear = ->
|
||||
switchClass( element, "", ["update-pending", "update-error", "update-success"], false )
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("ofn.admin").directive "ofnToggleColumn", ->
|
||||
angular.module("admin.indexUtils").directive "ofnToggleColumn", ->
|
||||
link: (scope, element, attrs) ->
|
||||
element.addClass "selected" if scope.column.visible
|
||||
element.click "click", ->
|
||||
@@ -8,4 +8,4 @@ angular.module("ofn.admin").directive "ofnToggleColumn", ->
|
||||
element.removeClass "selected"
|
||||
else
|
||||
scope.column.visible = true
|
||||
element.addClass "selected"
|
||||
element.addClass "selected"
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.indexUtils", ['ngResource']).config ($httpProvider) ->
|
||||
@@ -0,0 +1,8 @@
|
||||
angular.module("admin.indexUtils").factory 'Columns', ->
|
||||
new class Columns
|
||||
columns: {}
|
||||
|
||||
setColumns: (columns) ->
|
||||
@columns = {}
|
||||
@columns[name] = column for name, column of columns
|
||||
@columns
|
||||
@@ -0,0 +1,33 @@
|
||||
angular.module("admin.indexUtils").factory "pendingChanges", (resources) ->
|
||||
new class pendingChanges
|
||||
pendingChanges: {}
|
||||
|
||||
add: (id, attr, change) =>
|
||||
@pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}")
|
||||
@pendingChanges["#{id}"]["#{attr}"] = change
|
||||
|
||||
removeAll: =>
|
||||
@pendingChanges = {}
|
||||
|
||||
remove: (id, attr) =>
|
||||
if @pendingChanges.hasOwnProperty("#{id}")
|
||||
delete @pendingChanges["#{id}"]["#{attr}"]
|
||||
delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1
|
||||
|
||||
submitAll: =>
|
||||
all = []
|
||||
for id, objectChanges of @pendingChanges
|
||||
for attrName, change of objectChanges
|
||||
all.push @submit(change)
|
||||
all
|
||||
|
||||
submit: (change) ->
|
||||
resources.update(change).$promise.then (data) =>
|
||||
@remove change.object.id, change.attr
|
||||
change.scope.reset( data["#{change.attr}"] )
|
||||
change.scope.success()
|
||||
, (error) ->
|
||||
change.scope.error()
|
||||
|
||||
changeCount: (objectChanges) ->
|
||||
Object.keys(objectChanges).length
|
||||
@@ -0,0 +1,30 @@
|
||||
angular.module("admin.indexUtils").factory "resources", ($resource) ->
|
||||
LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json',
|
||||
{ order_number: '@order_number', line_item_id: '@line_item_id'},
|
||||
'update': { method: 'PUT' }
|
||||
Customer = $resource '/admin/customers/:customer_id.json',
|
||||
{ customer_id: '@customer_id'},
|
||||
'update': { method: 'PUT' }
|
||||
|
||||
return {
|
||||
update: (change) ->
|
||||
params = {}
|
||||
data = {}
|
||||
resource = null
|
||||
|
||||
switch change.type
|
||||
when "line_item"
|
||||
resource = LineItem
|
||||
params.order_number = change.object.order.number
|
||||
params.line_item_id = change.object.id
|
||||
data.line_item = {}
|
||||
data.line_item[change.attr] = change.value
|
||||
when "customer"
|
||||
resource = Customer
|
||||
params.customer_id = change.object.id
|
||||
data.customer = {}
|
||||
data.customer[change.attr] = change.value
|
||||
else ""
|
||||
|
||||
resource.update(params, data)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
angular.module("admin.indexUtils").factory "switchClass", ($timeout) ->
|
||||
return (element,classToAdd,removeClasses,timeout) ->
|
||||
$timeout.cancel element.timeout if element.timeout
|
||||
element.removeClass className for className in removeClasses
|
||||
element.addClass classToAdd
|
||||
intRegex = /^\d+$/
|
||||
if timeout && intRegex.test(timeout)
|
||||
element.timeout = $timeout(->
|
||||
element.removeClass classToAdd
|
||||
, timeout, true)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
angular.module("ofn.admin").factory "dataSubmitter", [
|
||||
"$http", "$q", "switchClass"
|
||||
($http, $q, switchClass) ->
|
||||
return (changeObj) ->
|
||||
deferred = $q.defer()
|
||||
$http.put(changeObj.url).success((data) ->
|
||||
switchClass changeObj.element, "update-success", ["update-pending", "update-error"], 3000
|
||||
deferred.resolve data
|
||||
).error ->
|
||||
switchClass changeObj.element, "update-error", ["update-pending", "update-success"], false
|
||||
deferred.reject()
|
||||
deferred.promise
|
||||
]
|
||||
@@ -3,12 +3,10 @@ angular.module("ofn.admin").factory "DisplayProperties", ->
|
||||
displayProperties: {}
|
||||
|
||||
showVariants: (product_id) ->
|
||||
@initProduct product_id
|
||||
@displayProperties[product_id].showVariants
|
||||
@productProperties(product_id).showVariants
|
||||
|
||||
setShowVariants: (product_id, showVariants) ->
|
||||
@initProduct product_id
|
||||
@displayProperties[product_id].showVariants = showVariants
|
||||
@productProperties(product_id).showVariants = showVariants
|
||||
|
||||
initProduct: (product_id) ->
|
||||
productProperties: (product_id) ->
|
||||
@displayProperties[product_id] ||= {showVariants: false}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
angular.module("ofn.admin").factory "pendingChanges",[
|
||||
"dataSubmitter"
|
||||
(dataSubmitter) ->
|
||||
pendingChanges: {}
|
||||
|
||||
add: (id, attrName, changeObj) ->
|
||||
@pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}")
|
||||
@pendingChanges["#{id}"]["#{attrName}"] = changeObj
|
||||
|
||||
removeAll: ->
|
||||
@pendingChanges = {}
|
||||
|
||||
remove: (id, attrName) ->
|
||||
if @pendingChanges.hasOwnProperty("#{id}")
|
||||
delete @pendingChanges["#{id}"]["#{attrName}"]
|
||||
delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1
|
||||
|
||||
submitAll: ->
|
||||
all = []
|
||||
for id,lineItem of @pendingChanges
|
||||
for attrName,changeObj of lineItem
|
||||
all.push @submit(id, attrName, changeObj)
|
||||
all
|
||||
|
||||
submit: (id, attrName, change) ->
|
||||
dataSubmitter(change).then (data) =>
|
||||
@remove id, attrName
|
||||
change.element.dbValue = data["#{attrName}"]
|
||||
|
||||
changeCount: (lineItem) ->
|
||||
Object.keys(lineItem).length
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
angular.module("ofn.admin").factory "switchClass", [
|
||||
"$timeout"
|
||||
($timeout) ->
|
||||
return (element,classToAdd,removeClasses,timeout) ->
|
||||
$timeout.cancel element.timeout if element.timeout
|
||||
element.removeClass className for className in removeClasses
|
||||
element.addClass classToAdd
|
||||
intRegex = /^\d+$/
|
||||
if timeout && intRegex.test(timeout)
|
||||
element.timeout = $timeout(->
|
||||
element.removeClass classToAdd
|
||||
, timeout, true)
|
||||
]
|
||||
@@ -1,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
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) ->
|
||||
$scope.Groups = Groups
|
||||
$scope.order = 'position'
|
||||
|
||||
#$rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) ->
|
||||
#$anchorScroll()
|
||||
#
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
1
app/assets/javascripts/shared/ng-tags-input.min.js
vendored
Executable file
1
app/assets/javascripts/shared/ng-tags-input.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -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()" } } ×
|
||||
|
||||
.small-12.large-8.columns
|
||||
|
||||
@@ -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 >
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,9 +38,13 @@
|
||||
%i.ofn-i_013-help
|
||||
|
||||
%p Producers make yummy things to eat &/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
|
||||
|
||||
%p If you’re not a producer, you’re 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" }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
*= require shared/jquery-ui-timepicker-addon
|
||||
*= require shared/textAngular.min
|
||||
*= require shared/ng-tags-input.min
|
||||
|
||||
*= require_self
|
||||
*= require_tree .
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
1
app/assets/stylesheets/shared/ng-tags-input.min.css
vendored
Executable file
1
app/assets/stylesheets/shared/ng-tags-input.min.css
vendored
Executable file
@@ -0,0 +1 @@
|
||||
tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0;padding-left:5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input.invalid-tag{color:red}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}
|
||||
29
app/controllers/admin/customers_controller.rb
Normal file
29
app/controllers/admin/customers_controller.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module Admin
|
||||
class CustomersController < ResourceController
|
||||
before_filter :load_managed_shops, only: :index, if: :html_request?
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: ActiveModel::ArraySerializer.new( @collection,
|
||||
each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user
|
||||
).to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection
|
||||
return Customer.where("1=0") if html_request? || params[:enterprise_id].nil?
|
||||
enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id])
|
||||
Customer.of(enterprise)
|
||||
end
|
||||
|
||||
def load_managed_shops
|
||||
@shops = Enterprise.managed_by(spree_current_user).is_distributor
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
class HomeController < BaseController
|
||||
layout 'darkswarm'
|
||||
before_filter :load_active_distributors
|
||||
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def about_us
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class MapController < BaseController
|
||||
layout 'darkswarm'
|
||||
before_filter :load_active_distributors
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class ProducersController < BaseController
|
||||
layout 'darkswarm'
|
||||
before_filter :load_active_distributors
|
||||
|
||||
|
||||
def index
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
class Customer < ActiveRecord::Base
|
||||
acts_as_taggable
|
||||
|
||||
belongs_to :enterprise
|
||||
belongs_to :user, :class_name => Spree.user_class
|
||||
|
||||
validates :code, presence: true, uniqueness: {scope: :enterprise_id}
|
||||
validates :email, presence: true
|
||||
validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true }
|
||||
validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" }
|
||||
validates :enterprise_id, presence: true
|
||||
|
||||
scope :of, ->(enterprise) { where(enterprise_id: enterprise) }
|
||||
|
||||
before_create :associate_user
|
||||
|
||||
private
|
||||
|
||||
def associate_user
|
||||
self.user = user || Spree::User.find_by_email(email)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@ class Enterprise < ActiveRecord::Base
|
||||
has_and_belongs_to_many :payment_methods, join_table: 'distributors_payment_methods', class_name: 'Spree::PaymentMethod', foreign_key: 'distributor_id'
|
||||
has_many :distributor_shipping_methods, foreign_key: :distributor_id
|
||||
has_many :shipping_methods, through: :distributor_shipping_methods
|
||||
has_many :customers
|
||||
|
||||
delegate :latitude, :longitude, :city, :state_name, :to => :address
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
.three.columns
|
||||
|
||||
14
app/serializers/api/admin/basic_order_cycle_serializer.rb
Normal file
14
app/serializers/api/admin/basic_order_cycle_serializer.rb
Normal 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
|
||||
11
app/serializers/api/admin/customer_serializer.rb
Normal file
11
app/serializers/api/admin/customer_serializer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class Api::Admin::CustomerSerializer < ActiveModel::Serializer
|
||||
attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list
|
||||
|
||||
def tag_list
|
||||
object.tag_list.join(",")
|
||||
end
|
||||
|
||||
def tags
|
||||
object.tag_list.map{ |t| { text: t } }
|
||||
end
|
||||
end
|
||||
19
app/serializers/api/admin/line_item_serializer.rb
Normal file
19
app/serializers/api/admin/line_item_serializer.rb
Normal 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
|
||||
31
app/serializers/api/admin/order_serializer.rb
Normal file
31
app/serializers/api/admin/order_serializer.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
3
app/serializers/api/admin/tax_category_serializer.rb
Normal file
3
app/serializers/api/admin/tax_category_serializer.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Api::Admin::TaxCategorySerializer < ActiveModel::Serializer
|
||||
attributes :id, :name
|
||||
end
|
||||
3
app/serializers/api/admin/units_product_serializer.rb
Normal file
3
app/serializers/api/admin/units_product_serializer.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Api::Admin::UnitsProductSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :group_buy_unit_size, :variant_unit
|
||||
end
|
||||
8
app/serializers/api/admin/units_variant_serializer.rb
Normal file
8
app/serializers/api/admin/units_variant_serializer.rb
Normal 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
|
||||
@@ -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" )
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
72
app/views/admin/customers/index.html.haml
Normal file
72
app/views/admin/customers/index.html.haml
Normal file
@@ -0,0 +1,72 @@
|
||||
- content_for :page_title do
|
||||
%h1.page-title Customers
|
||||
|
||||
= admin_inject_shops
|
||||
|
||||
%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } }
|
||||
.row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } }
|
||||
.five.columns.alpha
|
||||
%h3 Please select a Hub:
|
||||
.four.columns
|
||||
%select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' }
|
||||
.seven.columns.omega
|
||||
|
||||
.row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' }
|
||||
.controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" }
|
||||
.five.columns.alpha
|
||||
%input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' }
|
||||
.five.columns
|
||||
-# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true }
|
||||
-# %span{ :class => 'icon-check' } Actions
|
||||
-# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
-# %div.menu{ 'ng-show' => "expanded" }
|
||||
-# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true }
|
||||
-# %span{ :class => 'three columns omega' } {{action.name }}
|
||||
.three.columns
|
||||
.three.columns.omega
|
||||
%div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' }
|
||||
%span{ :class => 'icon-reorder' } Columns
|
||||
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
%div.menu{ 'ng-show' => "expanded" }
|
||||
%div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true }
|
||||
%span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }}
|
||||
%span{ :class => 'two columns omega' } {{column.name }}
|
||||
.row{ 'ng-if' => 'shop && !loaded()' }
|
||||
.sixteen.columns.alpha#loading
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg" }
|
||||
%h1 LOADING CUSTOMERS
|
||||
.row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'}
|
||||
%h1#no_results No customers found.
|
||||
|
||||
|
||||
.row{ ng: { show: "loaded() && filteredCustomers.length > 0" } }
|
||||
%form{ name: "customers" }
|
||||
%table.index#customers
|
||||
%col.email{ width: "20%"}
|
||||
%col.code{ width: "20%"}
|
||||
%col.tags{ width: "50%"}
|
||||
%col.actions{ width: "10%"}
|
||||
%thead
|
||||
%tr{ ng: { controller: "ColumnsCtrl" } }
|
||||
-# %th.bulk
|
||||
-# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" }
|
||||
%th.email{ 'ng-show' => 'columns.email.visible' }
|
||||
%a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email
|
||||
%th.code{ 'ng-show' => 'columns.code.visible' }
|
||||
%a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code
|
||||
%th.tags{ 'ng-show' => 'columns.tags.visible' } Tags
|
||||
%th.actions
|
||||
Ask?
|
||||
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
|
||||
%tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" }
|
||||
-# %td.bulk
|
||||
-# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' }
|
||||
%td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }}
|
||||
%td.code{ 'ng-show' => 'columns.code.visible' }
|
||||
%input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" }
|
||||
%td.tags{ 'ng-show' => 'columns.tags.visible' }
|
||||
.tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"}
|
||||
%tags_with_translation{ object: 'customer' }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" }
|
||||
%input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
%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
|
||||
%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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
-# %p.callout
|
||||
-# %strong
|
||||
-# Your enterprise details
|
||||
-# %table{:width => "100%"}
|
||||
-# %tr
|
||||
-# %td{:align => "right"}
|
||||
-# %strong
|
||||
-# Shop URL
|
||||
-# %td
|
||||
-# %td
|
||||
-# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"}
|
||||
-# = main_app.enterprise_shop_url(@enterprise)
|
||||
-# %tr
|
||||
-# %td
|
||||
-# %tr
|
||||
-# %td{:align => "right"}
|
||||
-# %strong
|
||||
-# Email
|
||||
-# %td
|
||||
-# %td
|
||||
-# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"}
|
||||
-# = @enterprise.email
|
||||
|
||||
%p
|
||||
%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
|
||||
%td
|
||||
%a{:href => "#{ spree.admin_url }", :target => "_blank"}
|
||||
= spree.admin_url
|
||||
|
||||
%p
|
||||
/ /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
Reference in New Issue
Block a user