diff --git a/Gemfile b/Gemfile index 0ed6cfc3f3..456234f554 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,11 @@ gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' gem 'spree_i18n', :github => 'spree/spree_i18n' gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-stable' -gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" + +# Waiting on merge of PR #117 +# https://github.com/spree-contrib/better_spree_paypal_express/pull/117 +gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable" +#gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" gem 'comfortable_mexican_sofa' diff --git a/Gemfile.lock b/Gemfile.lock index 4a5a94f0a8..9bb6e501db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,15 @@ GIT specs: custom_error_message (1.1.1) +GIT + remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git + revision: cdd61161ccd27cd8d183f9321422c7be113796b8 + branch: 1-3-stable + specs: + spree_paypal_express (2.0.3) + paypal-sdk-merchant (= 1.106.1) + spree_core (~> 1.3.4) + GIT remote: git://github.com/openfoodfoundation/spree.git revision: bbe5e779bcb883a1726ad4006d7c06b06c3f5372 @@ -54,15 +63,6 @@ GIT spree_sample (1.3.6.beta) spree_core (= 1.3.6.beta) -GIT - remote: git://github.com/spree-contrib/better_spree_paypal_express.git - revision: db135b89a289aaab951c1228bcc55871de0cbba7 - branch: 1-3-stable - specs: - spree_paypal_express (2.0.3) - paypal-sdk-merchant (= 1.106.1) - spree_core (~> 1.3.4) - GIT remote: git://github.com/spree/deface.git revision: 1110a1336252109bce7f98f9182042e0bc2930ae diff --git a/README.markdown b/README.markdown index 8975711f47..6ffde4df5c 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,6 @@ Supported by the Open Food Foundation, we are proudly open source and not-for-pr We're part of global movement - get involved! -* We're crowd-funding RIGHT NOW - please help out at http://startsomegood.com/openfoodnetwork * Fill in this short survey to tell us who you are and what you want to do with OFN: https://docs.google.com/a/eaterprises.com.au/forms/d/1zxR5vSiU9CigJ9cEaC8-eJLgYid8CR8er7PPH9Mc-30/edit# * Find out more and join in the conversation - http://openfoodnetwork.org diff --git a/app/assets/images/map-icon-both.svg b/app/assets/images/map-icon-both.svg index 08ffa25f6c..a51867f841 100644 --- a/app/assets/images/map-icon-both.svg +++ b/app/assets/images/map-icon-both.svg @@ -1,7 +1,7 @@ - diff --git a/app/assets/images/map-icon-hub.svg b/app/assets/images/map-icon-hub.svg index bb0e20aec4..fcd6a4913e 100644 --- a/app/assets/images/map-icon-hub.svg +++ b/app/assets/images/map-icon-hub.svg @@ -1,7 +1,7 @@ - diff --git a/app/assets/images/map-icon-producer.svg b/app/assets/images/map-icon-producer.svg index 1f2e4184a1..50ec792e91 100644 --- a/app/assets/images/map-icon-producer.svg +++ b/app/assets/images/map-icon-producer.svg @@ -1,7 +1,7 @@ - diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index a0235a568f..c80e6252de 100644 --- a/app/assets/javascripts/admin/admin.js.coffee +++ b/app/assets/javascripts/admin/admin.js.coffee @@ -1,3 +1,3 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown", "admin.products", "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, */*" \ No newline at end of file diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 4c763ec411..5ffb99ccb8 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -16,6 +16,7 @@ //= require admin/spree_auth //= require admin/spree_promo //= require admin/spree_paypal_express +//= require ../shared/ng-infinite-scroll.min.js //= require ./admin //= require ./enterprises/enterprises //= require ./payment_methods/payment_methods diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 8fc088fc97..35ec6e694c 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,23 +1,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ - "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", - ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager) -> + "$scope", "$timeout", "$http", "dataFetcher", "DirtyProducts", "VariantUnitManager", "producers", "Taxons", + ($scope, $timeout, $http, dataFetcher, DirtyProducts, VariantUnitManager, producers, Taxons) -> $scope.updateStatusMessage = text: "" style: {} $scope.columns = - supplier: {name: "Supplier", visible: true} + producer: {name: "Producer", visible: true} name: {name: "Name", visible: true} unit: {name: "Unit", visible: true} price: {name: "Price", visible: true} on_hand: {name: "On Hand", visible: true} - taxons: {name: "Taxons", visible: false} + category: {name: "Category", visible: false} available_on: {name: "Available On", visible: false} $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() $scope.filterableColumns = [ - { name: "Supplier", db_column: "supplier_name" }, + { name: "Producer", db_column: "producer_name" }, { name: "Name", db_column: "name" } ] @@ -28,25 +28,20 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.optionTabs = filters: { title: "Filter Products", visible: false } - column_toggle: { title: "Toggle Columns", visible: false } - $scope.perPage = 25 - $scope.currentPage = 1 + + $scope.producers = producers + $scope.taxons = Taxons.taxons + $scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers + $scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons + $scope.producerFilter = "0" + $scope.categoryFilter = "0" $scope.products = [] $scope.filteredProducts = [] $scope.currentFilters = [] - $scope.totalCount = -> $scope.filteredProducts.length - $scope.totalPages = -> Math.ceil($scope.totalCount()/$scope.perPage) - $scope.firstVisibleProduct = -> ($scope.currentPage-1)*$scope.perPage+1 - $scope.lastVisibleProduct = -> Math.min($scope.totalCount(),$scope.currentPage*$scope.perPage) - $scope.setPage = (page) -> $scope.currentPage = page - $scope.minPage = -> Math.max(1,Math.min($scope.totalPages()-4,$scope.currentPage-2)) - $scope.maxPage = -> Math.min($scope.totalPages(),Math.max(5,$scope.currentPage+2)) + $scope.limit = 15 + $scope.productsWithUnsavedVariants = [] - $scope.$watch -> - $scope.totalPages() - , (newVal, oldVal) -> - $scope.currentPage = Math.max $scope.totalPages(), 1 if newVal != oldVal && $scope.totalPages() < $scope.currentPage $scope.initialise = (spree_api_key) -> authorise_api_reponse = "" @@ -55,24 +50,29 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised" if $scope.spree_api_key_ok $http.defaults.headers.common["X-Spree-Token"] = spree_api_key - dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> - $scope.suppliers = data - # Need to have suppliers before we get products so we can match suppliers to product.supplier - $scope.fetchProducts() + $scope.fetchProducts() else if authorise_api_reponse.hasOwnProperty("error") $scope.api_error_msg = authorise_api_reponse("error") else api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." + $scope.$watchCollection '[query, producerFilter, categoryFilter]', -> + $scope.limit = 15 # Reset limit whenever searching $scope.fetchProducts = -> # WARNING: returns a promise $scope.loading = true queryString = $scope.currentFilters.reduce (qs,f) -> return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};" , "" - return dataFetcher("/api/products/managed?template=bulk_index;page=1;per_page=500;#{queryString}").then (data) -> - $scope.resetProducts data + return dataFetcher("/api/products/bulk_products?page=1;per_page=20;#{queryString}").then (data) -> + $scope.resetProducts data.products $scope.loading = false + if data.pages > 1 + for page in [2..data.pages] + dataFetcher("/api/products/bulk_products?page=#{page};per_page=20;#{queryString}").then (data) -> + for product in data.products + $scope.unpackProduct product + $scope.products.push product $scope.resetProducts = (data) -> @@ -87,17 +87,15 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.unpackProduct = (product) -> $scope.displayProperties ||= {} $scope.displayProperties[product.id] ||= showVariants: false - $scope.matchSupplier product + #$scope.matchProducer product $scope.loadVariantUnit product - $scope.matchSupplier = (product) -> - for i of $scope.suppliers - supplier = $scope.suppliers[i] - if angular.equals(supplier, product.supplier) - product.supplier = supplier - break - + # $scope.matchProducer = (product) -> + # for producer in $scope.producers + # if angular.equals(producer.id, product.producer) + # product.producer = producer + # break $scope.loadVariantUnit = (product) -> product.variant_unit_with_scale = @@ -108,13 +106,14 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ else null - if product.variants - for variant in product.variants - $scope.loadVariantVariantUnit product, variant - $scope.loadVariantVariantUnit product, product.master if product.master + $scope.loadVariantUnitValues product if product.variants + $scope.loadVariantUnitValue product, product.master if product.master + $scope.loadVariantUnitValues = (product) -> + for variant in product.variants + $scope.loadVariantUnitValue product, variant - $scope.loadVariantVariantUnit = (product, variant) -> + $scope.loadVariantUnitValue = (product, variant) -> unit_value = $scope.variantUnitValue product, variant unit_value = if unit_value? then unit_value else '' variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() @@ -153,29 +152,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ tab.visible = !tab.visible $scope.visibleTab = tab - $scope.addFilter = (filter) -> - existingfilterIndex = $scope.indexOfFilter filter - if $scope.filterableColumns.indexOf(filter.property) >= 0 && $scope.filterTypes.indexOf(filter.predicate) >= 0 && filter.value != "" && filter.value != undefined - if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0) - if existingfilterIndex == -1 - $scope.currentFilters.push filter - $scope.fetchProducts() - else if confirm("'#{filter.predicate.name}' filter already exists on column '#{filter.property.name}'. Replace it?") - $scope.currentFilters[existingfilterIndex] = filter - $scope.fetchProducts() - else - alert("Please ensure all filter fields are filled in before adding a filter.") - - $scope.removeFilter = (filter) -> - index = $scope.currentFilters.indexOf(filter) - if index != -1 - $scope.currentFilters.splice index, 1 - $scope.fetchProducts() - - $scope.indexOfFilter = (filter) -> - for existingFilter, i in $scope.currentFilters - return i if filter.property == existingFilter.property && filter.predicate == existingFilter.predicate - return -1 + $scope.resetSelectFilters = -> + $scope.query = "" + $scope.producerFilter = "0" + $scope.categoryFilter = "0" $scope.editWarn = (product, variant) -> if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0) @@ -192,6 +172,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ display_name: null on_hand: null price: null + $scope.productsWithUnsavedVariants.push product $scope.displayProperties[product.id].showVariants = true @@ -200,6 +181,11 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.variantIdCounter -= 1 $scope.variantIdCounter + $scope.updateVariantLists = (server_products) -> + for product in $scope.productsWithUnsavedVariants + server_product = $scope.findProduct(product.id, server_products) + product.variants = server_product.variants + $scope.loadVariantUnitValues product $scope.deleteProduct = (product) -> if confirm("Are you sure?") @@ -244,7 +230,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.hasVariants = (product) -> - Object.keys(product.variants).length > 0 + product.variants.length > 0 $scope.hasUnit = (product) -> @@ -269,7 +255,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if productsToSubmit.length > 0 $scope.updateProducts productsToSubmit # Don't submit an empty list else - $scope.setMessage $scope.updateStatusMessage, "No changes to update.", color: "grey", 3000 + $scope.setMessage $scope.updateStatusMessage, "No changes to save.", color: "grey", 3000 $scope.updateProducts = (productsToSubmit) -> @@ -281,22 +267,16 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ products: productsToSubmit filters: $scope.currentFilters ).success((data) -> - # TODO: remove this check altogether, need to write controller tests if we want to test this behaviour properly - # Note: Rob implemented subset(), which is a simpler alternative to productsWithoutDerivedAttributes(). However, it - # conflicted with some changes I made before merging my work, so for now I've reverted to the old way of - # doing things. TODO: Review together and decide on strategy here. -- Rohan, 14-1-2014 - #if subset($scope.productsWithoutDerivedAttributes(), data) - if $scope.productListsMatch $scope.products, data - $scope.resetProducts data - $timeout -> $scope.displaySuccess() - else - # console.log angular.toJson($scope.productsWithoutDerivedAttributes($scope.products)) - # console.log "---" - # console.log angular.toJson($scope.productsWithoutDerivedAttributes(data)) - # console.log "---" - $scope.displayFailure "Product lists do not match." + DirtyProducts.clear() + $scope.updateVariantLists(data.products) + $timeout -> $scope.displaySuccess() ).error (data, status) -> - $scope.displayFailure "Server returned with error status: " + status + if status == 400 && data.errors? && data.errors.length > 0 + errors = error + "\n" for error in data.errors + alert "Saving failed with the following error(s):\n" + errors + $scope.displayFailure "Save failed due to invalid data" + else + $scope.displayFailure "Server returned with error status: " + status $scope.packProduct = (product) -> @@ -322,58 +302,19 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ if variant.hasOwnProperty("unit_value_with_description") match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/) if match - product = $scope.findProduct(product.id) + product = $scope.findProduct(product.id, $scope.products) variant.unit_value = parseFloat(match[1]) variant.unit_value = null if isNaN(variant.unit_value) variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale variant.unit_description = match[3] - - $scope.productListsMatch = (clientProducts, serverProducts) -> - $scope.copyNewVariantIds clientProducts, serverProducts - angular.toJson($scope.productsWithoutDerivedAttributes(clientProducts)) == angular.toJson($scope.productsWithoutDerivedAttributes(serverProducts)) - - - # When variants are created clientside, they are given a negative id. The server - # responds with a real id, which would cause the productListsMatch() check to fail. - # To avoid that false negative, we copy the server variant id to the client for any - # negative ids. - $scope.copyNewVariantIds = (clientProducts, serverProducts) -> - if clientProducts? - for product, i in clientProducts - if product.variants? - for variant, j in product.variants - if variant.id < 0 - variant.id = serverProducts[i].variants[j].id - - - $scope.productsWithoutDerivedAttributes = (products) -> - products_filtered = [] - if products - products_filtered = $scope.deepCopyProducts products - for product in products_filtered - delete product.variant_unit_with_scale - if product.variants - for variant in product.variants - delete variant.unit_value_with_description - # If we end up live-updating this field, we might want to reinstate its verification here - delete variant.options_text - delete product.master - products_filtered - - - $scope.deepCopyProducts = (products) -> - copied_products = (angular.extend {}, product for product in products) - for product in copied_products - if product.variants - product.variants = (angular.extend {}, variant for variant in product.variants) - copied_products - - - $scope.findProduct = (id) -> - products = (product for product in $scope.products when product.id == id) + $scope.findProduct = (id, product_list) -> + products = (product for product in product_list when product.id == id) if products.length == 0 then null else products[0] + $scope.incrementLimit = -> + if $scope.limit < $scope.products.length + $scope.limit = $scope.limit + 5 $scope.setMessage = (model, text, style, timeout) -> model.text = text @@ -386,26 +327,27 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", [ $scope.displayUpdating = -> - $scope.setMessage $scope.updateStatusMessage, "Updating...", - color: "orange" + $scope.setMessage $scope.updateStatusMessage, "Saving...", + color: "#FF9906" , false $scope.displaySuccess = -> - $scope.setMessage $scope.updateStatusMessage, "Update complete", - color: "green" + $scope.setMessage $scope.updateStatusMessage, "Changes saved.", + color: "#9fc820" , 3000 $scope.displayFailure = (failMessage) -> - $scope.setMessage $scope.updateStatusMessage, "Updating failed. " + failMessage, - color: "red" - , 10000 + $scope.setMessage $scope.updateStatusMessage, "Saving failed. " + failMessage, + color: "#DA5354" + , false $scope.displayDirtyProducts = -> if DirtyProducts.count() > 0 - $scope.setMessage $scope.updateStatusMessage, "Changes to " + DirtyProducts.count() + " products remain unsaved.", + message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products" + $scope.setMessage $scope.updateStatusMessage, "Changes to " + message + " remain unsaved.", color: "gray" , false else @@ -442,8 +384,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("name") filteredProduct.name = product.name hasUpdatableProperty = true - if product.hasOwnProperty("supplier") - filteredProduct.supplier_id = product.supplier.id + if product.hasOwnProperty("producer") + filteredProduct.supplier_id = product.producer hasUpdatableProperty = true if product.hasOwnProperty("price") filteredProduct.price = product.price @@ -458,8 +400,8 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present filteredProduct.on_hand = product.on_hand hasUpdatableProperty = true - if product.hasOwnProperty("taxon_ids") - filteredProduct.taxon_ids = product.taxon_ids + if product.hasOwnProperty("category") + filteredProduct.primary_taxon_id = product.category hasUpdatableProperty = true if product.hasOwnProperty("available_on") filteredProduct.available_on = product.available_on @@ -510,11 +452,3 @@ toObjectWithIDKeys = (array) -> object[array[i].id].variants = toObjectWithIDKeys(array[i].variants) if array[i].hasOwnProperty("variants") and array[i].variants instanceof Array object - -subset = (bigArray,smallArray) -> - if smallArray instanceof Array && bigArray instanceof Array && smallArray.length > 0 - for item in smallArray - return false if angular.toJson(bigArray).indexOf(angular.toJson(item)) == -1 - return true - else - return false diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee index 665753a522..88524fb330 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee @@ -1,9 +1,10 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($scope, EnterpriseRelationships, Enterprises) -> $scope.EnterpriseRelationships = EnterpriseRelationships $scope.Enterprises = Enterprises + $scope.permissions = {} $scope.create = -> - $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id) + $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id, $scope.permissions) $scope.delete = (enterprise_relationship) -> if confirm("Are you sure?") diff --git a/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee new file mode 100644 index 0000000000..026913a263 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/enterprise_roles_controller.js.coffee @@ -0,0 +1,11 @@ +angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, EnterpriseRoles, Users, Enterprises) -> + $scope.EnterpriseRoles = EnterpriseRoles + $scope.Users = Users + $scope.Enterprises = Enterprises + + $scope.create = -> + $scope.EnterpriseRoles.create($scope.user_id, $scope.enterprise_id) + + $scope.delete = (enterprise_role) -> + if confirm("Are you sure?") + $scope.EnterpriseRoles.delete enterprise_role diff --git a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee new file mode 100644 index 0000000000..3b3378d013 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/providers_controller.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").controller "ProvidersCtrl", ($scope, paymentMethod) -> + if paymentMethod.type + $scope.include_html = "/admin/payment_methods/show_provider_preferences?" + + "provider_type=#{paymentMethod.type};" + + "pm_id=#{paymentMethod.id};" + else + $scope.include_html = "" \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee b/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee new file mode 100644 index 0000000000..467bad4e5f --- /dev/null +++ b/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").directive "providerPrefsFor", ($http) -> + link: (scope, element, attrs) -> + element.on "change blur load", -> + scope.$apply -> + scope.include_html = "/admin/payment_methods/show_provider_preferences?" + + "provider_type=#{element.val()};" + + "pm_id=#{attrs.providerPrefsFor};" \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee index e5713274ef..5f17a0d1dd 100644 --- a/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/directives/taxon_autocomplete.js.coffee @@ -4,18 +4,16 @@ angular.module("ofn.admin").directive "ofnTaxonAutocomplete", (Taxons) -> link: (scope,element,attrs,ngModel) -> setTimeout -> element.select2 - placeholder: Spree.translations.taxon_placeholder - multiple: true + placeholder: "Category" + multiple: false initSelection: (element, callback) -> - Taxons.findByIDs(element.val()).$promise.then (result) -> - callback Taxons.cleanTaxons(result) + callback Taxons.findByID(scope.product.category) query: (query) -> - Taxons.findByTerm(query.term).$promise.then (result) -> - query.callback { results: Taxons.cleanTaxons(result) } + query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> - taxon.pretty_name + taxon.name formatSelection: (taxon) -> - taxon.pretty_name + taxon.name element.on "change", -> scope.$apply -> ngModel.$setViewValue element.val() \ No newline at end of file diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index cf182df494..c5b38191ba 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -4,8 +4,8 @@ angular.module("admin.enterprises") $scope.PaymentMethods = PaymentMethods.paymentMethods $scope.ShippingMethods = ShippingMethods.shippingMethods - for PaymentMethod in $scope.PaymentMethods - PaymentMethod.selected = if PaymentMethod.id in $scope.Enterprise.payment_method_ids then true else false + for payment_method in $scope.PaymentMethods + payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids $scope.paymentMethodsColor = -> if $scope.PaymentMethods.length > 0 @@ -14,13 +14,13 @@ angular.module("admin.enterprises") "red" $scope.selectedPaymentMethodsCount = -> - $scope.PaymentMethods.reduce (count, PaymentMethod) -> - count++ if PaymentMethod.selected + $scope.PaymentMethods.reduce (count, payment_method) -> + count++ if payment_method.selected count , 0 - for ShippingMethod in $scope.ShippingMethods - ShippingMethod.selected = if ShippingMethod.id in $scope.Enterprise.shipping_method_ids then true else false + for shipping_method in $scope.ShippingMethods + shipping_method.selected = shipping_method.id in $scope.Enterprise.shipping_method_ids $scope.shippingMethodsColor = -> if $scope.ShippingMethods.length > 0 @@ -29,7 +29,7 @@ angular.module("admin.enterprises") "red" $scope.selectedShippingMethodsCount = -> - $scope.ShippingMethods.reduce (count, ShippingMethod) -> - count++ if ShippingMethod.selected + $scope.ShippingMethods.reduce (count, shipping_method) -> + count++ if shipping_method.selected count , 0 \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/category_filter.js.coffee b/app/assets/javascripts/admin/filters/category_filter.js.coffee new file mode 100644 index 0000000000..b89e706815 --- /dev/null +++ b/app/assets/javascripts/admin/filters/category_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "category", ($filter) -> + return (products, taxonID) -> + return products if taxonID == "0" + return $filter('filter')( products, { category: taxonID }, true ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/producer_filter.js.coffee b/app/assets/javascripts/admin/filters/producer_filter.js.coffee new file mode 100644 index 0000000000..7325b2200d --- /dev/null +++ b/app/assets/javascripts/admin/filters/producer_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "producer", ($filter) -> + return (products, producerID) -> + return products if producerID == "0" + $filter('filter')( products, { producer: producerID }, true ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee b/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee new file mode 100644 index 0000000000..78a54175bf --- /dev/null +++ b/app/assets/javascripts/admin/filters/taxons_term_filter.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").filter "taxonsTermFilter", -> + return (lineItems,selectedSupplier,selectedDistributor,selectedOrderCycle) -> + filtered = [] + filtered.push lineItem for lineItem in lineItems when (angular.equals(selectedSupplier,"0") || lineItem.supplier.id == selectedSupplier) && + (angular.equals(selectedDistributor,"0") || lineItem.order.distributor.id == selectedDistributor) && + (angular.equals(selectedOrderCycle,"0") || lineItem.order.order_cycle.id == selectedOrderCycle) + filtered \ No newline at end of file diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index aadf66af60..b8068681ca 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -330,7 +330,7 @@ angular.module('order_cycle', ['ngResource']) }]) .factory('Enterprise', ['$resource', ($resource) -> - Enterprise = $resource('/admin/enterprises/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) + Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) { Enterprise: Enterprise diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index e07b992112..cad556efd8 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -1,12 +1,17 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterprise_relationships) -> new class EnterpriseRelationships create_errors: "" + all_permissions: [ + 'add_to_order_cycle' + 'manage_products' + ] constructor: -> @enterprise_relationships = enterprise_relationships - create: (parent_id, child_id) -> - $http.post('/admin/enterprise_relationships', {enterprise_relationship: {parent_id: parent_id, child_id: child_id}}).success (data, status) => + create: (parent_id, child_id, permissions) -> + permissions = (name for name, enabled of permissions when enabled) + $http.post('/admin/enterprise_relationships', {enterprise_relationship: {parent_id: parent_id, child_id: child_id, permissions_list: permissions}}).success (data, status) => @enterprise_relationships.unshift(data) @create_errors = "" @@ -16,3 +21,8 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris delete: (er) -> $http.delete('/admin/enterprise_relationships/' + er.id).success (data) => @enterprise_relationships.splice @enterprise_relationships.indexOf(er), 1 + + permission_presentation: (permission) -> + switch permission + when "add_to_order_cycle" then "can add to order cycle" + when "manage_products" then "can manage the products of" diff --git a/app/assets/javascripts/admin/services/enterprise_roles.js.coffee b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee new file mode 100644 index 0000000000..ca11ab33c6 --- /dev/null +++ b/app/assets/javascripts/admin/services/enterprise_roles.js.coffee @@ -0,0 +1,18 @@ +angular.module("ofn.admin").factory 'EnterpriseRoles', ($http, enterpriseRoles) -> + new class EnterpriseRoles + create_errors: "" + + constructor: -> + @enterprise_roles = enterpriseRoles + + create: (user_id, enterprise_id) -> + $http.post('/admin/enterprise_roles', {enterprise_role: {user_id: user_id, enterprise_id: enterprise_id}}).success (data, status) => + @enterprise_roles.unshift(data) + @create_errors = "" + + .error (response, status) => + @create_errors = response.errors + + delete: (er) -> + $http.delete('/admin/enterprise_roles/' + er.id).success (data) => + @enterprise_roles.splice @enterprise_roles.indexOf(er), 1 diff --git a/app/assets/javascripts/admin/services/taxons.js.coffee b/app/assets/javascripts/admin/services/taxons.js.coffee index 1779ec3518..6944fe132f 100644 --- a/app/assets/javascripts/admin/services/taxons.js.coffee +++ b/app/assets/javascripts/admin/services/taxons.js.coffee @@ -1,13 +1,15 @@ -angular.module("ofn.admin").factory "Taxons", ($resource) -> - resource = $resource "/admin/taxons/search" +angular.module("ofn.admin").factory "Taxons", (taxons, $filter) -> + new class Taxons + constructor: -> + @taxons = taxons - return { + # For finding a single Taxon + findByID: (id) -> + $filter('filter')(@taxons, {id: id}, true)[0] + + # For finding multiple Taxons represented by comma delimited string findByIDs: (ids) -> - resource.get { ids: ids } + taxon for taxon in @taxons when taxon.id.toString() in ids.split(",") findByTerm: (term) -> - resource.get { q: term } - - cleanTaxons: (data) -> - data['taxons'].map (result) -> result - } \ No newline at end of file + $filter('filter')(@taxons, term) \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/users.js.coffee b/app/assets/javascripts/admin/services/users.js.coffee new file mode 100644 index 0000000000..638908542e --- /dev/null +++ b/app/assets/javascripts/admin/services/users.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").factory 'Users', (users) -> + new class Users + constructor: -> + @users = users diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 45f91366ba..f529ac3255 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -11,7 +11,6 @@ #= require lodash.underscore.js #= require angular-scroll.min.js #= require angular-google-maps.min.js -#= require angular-timer.min.js #= require ../shared/mm-foundation-tpls-0.2.2.min.js #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee index c24f7a70f2..144cf0dfef 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee @@ -1,13 +1,13 @@ -Darkswarm.controller "LoginCtrl", ($scope, $http, AuthenticationService, Redirections, Loading) -> +Darkswarm.controller "LoginCtrl", ($scope, $http, $window, AuthenticationService, Redirections, Loading) -> $scope.path = "/login" $scope.submit = -> Loading.message = "Hold on a moment, we're logging you in" $http.post("/user/spree_user/sign_in", {spree_user: $scope.spree_user}).success (data)-> if Redirections.after_login - location.href = location.origin + Redirections.after_login + $window.location.href = $window.location.origin + Redirections.after_login else - location.href = location.origin + location.pathname # Strips out hash fragments + $window.location.href = $window.location.origin + $window.location.pathname # Strips out hash fragments .error (data) -> Loading.clear() $scope.errors = data.message diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index a5966e503b..e77712eae6 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -3,7 +3,7 @@ Darkswarm.controller "CheckoutCtrl", ($scope, storage, Checkout, CurrentUser, Cu # Bind to local storage $scope.fieldsToBind = ["bill_address", "email", "payment_method_id", "shipping_method_id", "ship_address"] - prefix = "order_#{Checkout.order.id}#{Checkout.order.user_id}#{CurrentHub.hub.id}" + prefix = "order_#{Checkout.order.id}#{CurrentUser?.id}#{CurrentHub.hub.id}" for field in $scope.fieldsToBind storage.bind $scope, "Checkout.order.#{field}", diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 9ae7c14b90..71f893aa64 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -6,7 +6,7 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> {key: "January", value: "1"}, {key: "February", value: "2"}, {key: "March", value: "3"}, - {key: "April", value: "4"}, + {key: "April", value: "4"}, {key: "May", value: "5"}, {key: "June", value: "6"}, {key: "July", value: "7"}, @@ -20,4 +20,4 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> $scope.years = [moment().year()..(moment().year()+15)] $scope.secrets.card_month = "1" $scope.secrets.card_year = moment().year() - $timeout $scope.onTimeout + $timeout $scope.onTimeout diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index da595418da..8ed56f252d 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -6,7 +6,8 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, $scope.filterText = FilterSelectorsService.filterText $scope.FilterSelectorsService = FilterSelectorsService $scope.limit = 3 - $scope.ordering = {order: "name"} + $scope.ordering = + order: "primary_taxon.name" $scope.order_cycle = OrderCycle.order_cycle $scope.incrementLimit = -> diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 7a6cba7070..1e58fe7294 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -5,7 +5,6 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'infinite-scroll', 'angular-flash.service', 'templates', - 'timer', 'ngSanitize', 'ngAnimate', 'google-maps', diff --git a/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee index 74523c2c3d..563b5a1716 100644 --- a/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/active_selector.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "activeSelector", -> + # A generic selector that allows an object/scope to be toggled between active and inactive + # Used in the filters, but hypothetically useable anywhere restrict: 'E' transclude: true replace: true @@ -8,5 +10,6 @@ Darkswarm.directive "activeSelector", -> elem.bind "click", -> scope.$apply -> scope.selector.active = !scope.selector.active - scope.emit() + # This function is a convention, e.g. a callback on the scope applied when active changes + scope.emit() if scope.emit diff --git a/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee b/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee index 4369fced34..994a7ced48 100644 --- a/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee @@ -1,11 +1,12 @@ Darkswarm.directive "activeTableHubLink", (CurrentHub, CurrentOrder) -> + # Change the text of the hub link based on CurrentHub + # To be used with ofnEmptiesCart + # Takes "change" and "shop" as text string attributes restrict: "A" scope: hub: '=activeTableHubLink' template: "{{action}}" link: (scope, elm, attr)-> - # Swap out the text of the hub link depending on whether it'll change current hub - # To be used with ofnEmptiesCart if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id scope.action = attr.change else diff --git a/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee b/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee index bcc0c8bc08..1ef1b3f9c3 100644 --- a/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/cart_popover.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "cart", -> + # Toggles visibility of the "cart" popover restrict: 'A' link: (scope, elem, attr)-> scope.open = false diff --git a/app/assets/javascripts/darkswarm/directives/debounce.js.coffee b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee index 343fcb531a..80f55de1d7 100644 --- a/app/assets/javascripts/darkswarm/directives/debounce.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "ngDebounce", ($timeout) -> + # Slows down ng-model updates, only triggering binding ngDebounce milliseconds + # after the last change. Used to prevent squirrely UI restrict: "A" require: "ngModel" priority: 99 diff --git a/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee index 352ed85fd6..ce1178e15e 100644 --- a/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "ofnDisableEnter", ()-> + # Stops enter from doing normal enter things restrict: 'A' link: (scope, element, attrs)-> element.bind "keydown keypress", (e)-> diff --git a/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee index 7c870ab860..40b8230e65 100644 --- a/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee @@ -1,6 +1,7 @@ Darkswarm.directive "ofnDisableScroll", ()-> + # Stops scrolling from incrementing or decrementing input value + # Useful for number inputs restrict: 'A' - link: (scope, element, attrs)-> element.bind 'focus', -> element.bind 'mousewheel', (e)-> diff --git a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee index 75e88f3682..b49131afa6 100644 --- a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee @@ -1,12 +1,13 @@ -Darkswarm.directive "ofnEmptiesCart", (CurrentHub, CurrentOrder, Navigation, storage) -> +Darkswarm.directive "ofnEmptiesCart", (CurrentHub, Cart, Navigation, storage) -> + # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, + # and Cart isn't empty restrict: "A" + scope: + hub: "=ofnEmptiesCart" link: (scope, elm, attr)-> - hub = scope.$eval(attr.ofnEmptiesCart) - # A hub is selected, we're changing to a different hub, and the cart isn't empty - if CurrentHub.hub?.id and CurrentHub.hub.id isnt hub.id - unless CurrentOrder.empty() - elm.bind 'click', (ev)-> - ev.preventDefault() - if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." - storage.clearAll() # One day this will have to be moar GRANULAR - Navigation.go scope.hub.path + if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() + elm.bind 'click', (ev)-> + ev.preventDefault() + if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." + storage.clearAll() # One day this will have to be moar GRANULAR + Navigation.go scope.hub.path diff --git a/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee b/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee index a8a2fbeebc..c9d05428ce 100644 --- a/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/fill_vertical.js.coffee @@ -1,10 +1,9 @@ Darkswarm.directive "fillVertical", ($window)-> + # Makes something fill the window vertically. Used on the Google Map. restrict: 'A' - link: (scope, element, attrs)-> setSize = -> element.css "height", ($window.innerHeight - element.offset().top) setSize() - angular.element($window).bind "resize", -> setSize() diff --git a/app/assets/javascripts/darkswarm/directives/flash.js.coffee b/app/assets/javascripts/darkswarm/directives/flash.js.coffee index b5c9aaddd8..74f6571f73 100644 --- a/app/assets/javascripts/darkswarm/directives/flash.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/flash.js.coffee @@ -1,5 +1,6 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> - # Mappings between flash types (left) and Foundation classes + # Our own flash class. Uses the "flash" service (third party), and a directive + # called RailsFlashLoader to render typePairings = info: "info" error: "alert" @@ -13,6 +14,8 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> link: ($scope, element, attr) -> $scope.flashes = [] + + # Callback when a new flash message is pushed to flash service show = (message, type)=> if message $scope.flashes.push({message: message, type: typePairings[type]}) @@ -21,5 +24,6 @@ Darkswarm.directive "ofnFlash", (flash, $timeout, RailsFlashLoader)-> $scope.delete = -> $scope.flashes.shift() + # Register our callback (above) with flash service flash.subscribe(show) RailsFlashLoader.initFlash() diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee index c481702d6c..3c3bb4f05a 100644 --- a/app/assets/javascripts/darkswarm/directives/focus.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -1,9 +1,9 @@ Darkswarm.directive "ofnFocus", -> + # Takes an expression attrs.ofnFocus + # Watches value of expression, triggers element.focus() when value is truthy + # Used to automatically focus on specific inputs in various circumstances restrict: "A" link: (scope, element, attrs) -> scope.$watch attrs.ofnFocus, ((focus) -> focus and element.focus() - return ), true - - return diff --git a/app/assets/javascripts/darkswarm/directives/loading.js.coffee b/app/assets/javascripts/darkswarm/directives/loading.js.coffee index febe56de9b..c7191b4fe2 100644 --- a/app/assets/javascripts/darkswarm/directives/loading.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/loading.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "loading", (Loading)-> + # Triggers a screen-wide "loading" thing when Ajaxy stuff is happening scope: {} restrict: 'E' templateUrl: 'loading.html' @@ -6,5 +7,3 @@ Darkswarm.directive "loading", (Loading)-> $scope.Loading = Loading $scope.show = -> $scope.Loading.message? - - link: ($scope, element, attr)-> diff --git a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee index 4942032580..88456db2a3 100644 --- a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee @@ -3,7 +3,7 @@ Darkswarm.directive 'mapSearch', ($timeout)-> restrict: 'E' require: '^googleMap' replace: true - template: '' + template: '' link: (scope, elem, attrs, ctrl)-> $timeout => map = ctrl.getMap() diff --git a/app/assets/javascripts/darkswarm/directives/max.js.coffee b/app/assets/javascripts/darkswarm/directives/max.js.coffee new file mode 100644 index 0000000000..ed3d7b3253 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/max.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "max", -> + restrict: 'A' + link: (scope, elem, attr)-> + elem.bind 'input', -> + if elem.val() > +attr.max + elem.val attr.max diff --git a/app/assets/javascripts/darkswarm/directives/modal.js.coffee b/app/assets/javascripts/darkswarm/directives/modal.js.coffee index d727fa64c9..7c1babe215 100644 --- a/app/assets/javascripts/darkswarm/directives/modal.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/modal.js.coffee @@ -1,16 +1,19 @@ Darkswarm.directive "ofnModal", ($modal)-> + # Generic modal! Uses transclusion so designer-types can do stuff like: + # %ofn-modal + # CONTENT + # Only works for simple cases, so roll your own when necessary! restrict: 'E' replace: true transclude: true - scope: {} + scope: true template: "{{title}}" + # Instead of using ng-transclude we compile the transcluded template to a string + # This compiled template is sent to the $modal service! Such magic! + # In theory we could compile the template directly inside link rather than onclick, but it's performant so meh! link: (scope, elem, attrs, ctrl, transclude)-> scope.title = attrs.title - contents = null elem.on "click", => - # We're using an isolate scope, which is a child of the original scope - # We have to compile the transclude against the original scope, not the isolate - transclude scope.$parent, (clone)-> - contents = clone - scope.modalInstance = $modal.open(controller: ctrl, template: contents, scope: scope.$parent) + transclude scope, (clone)-> + scope.modalInstance = $modal.open(controller: ctrl, template: clone, scope: scope) diff --git a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee index 4875cf81ec..8b0beb17a5 100644 --- a/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/price_breakdown.js.coffee @@ -1,11 +1,20 @@ Darkswarm.directive "priceBreakdown", ($tooltip)-> - tooltip = $tooltip 'priceBreakdown', 'priceBreakdown', 'click' + # We use the $tooltip service from Angular foundation to give us boilerplate + # Subsequently we patch the scope, template and restrictions + tooltip = $tooltip 'priceBreakdown', 'priceBreakdown', 'click' tooltip.scope = variant: "=" + tooltip.templateUrl = "price_breakdown_button.html" + tooltip.replace = true + tooltip.restrict = 'E' tooltip +# This is automatically referenced via naming convention in $tooltip Darkswarm.directive 'priceBreakdownPopup', -> restrict: 'EA' replace: true templateUrl: 'price_breakdown.html' - scope: true + scope: false + + link: (scope, elem, attrs) -> + scope.expanded = false unless scope.expanded? diff --git a/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee b/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee new file mode 100644 index 0000000000..35140598c4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/price_percentage.js.coffee @@ -0,0 +1,10 @@ +Darkswarm.directive "pricePercentage", -> + restrict: 'E' + replace: true + templateUrl: 'price_percentage.html' + scope: + percentage: '=' + + link: (scope, elem, attrs) -> + elem.find(".meter").css + width: "#{scope.percentage}%" diff --git a/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee b/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee index 86ffea8c5b..c53448473d 100644 --- a/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/render_svg.js.coffee @@ -1,7 +1,11 @@ Darkswarm.directive "renderSvg", ()-> + # Magical directive that'll render SVGs from URLs + # If only there were a neater way of doing this restrict: 'E' priority: 99 template: "" + + # Fetch SVG via ajax, inject into page using DOM link: (scope, elem, attr)-> if /.svg/.test attr.path # Only do this if we've got an svg $.ajax diff --git a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee index ab2e0a6e3d..55c5a311da 100644 --- a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive 'scrollAfterLoad', ($timeout, $location, $document)-> + # Scroll to an element on page load restrict: "A" link: (scope, element, attr) -> if scope.$last is true diff --git a/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee b/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee index 3db3446596..a63337dc70 100644 --- a/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scrollto.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "ofnScrollTo", ($location, $anchorScroll)-> + # Onclick sets $location.hash to attrs.ofnScrollTo + # Then triggers anchorScroll restrict: 'A' link: (scope, element, attrs)-> element.bind 'click', (ev)-> diff --git a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee index 3f752f8a3d..07656d84b7 100644 --- a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee @@ -1,4 +1,5 @@ Darkswarm.directive "shippingTypeSelector", (FilterSelectorsService)-> + # Builds selector for shipping types restrict: 'E' replace: true templateUrl: 'shipping_type_selector.html' diff --git a/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee b/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee new file mode 100644 index 0000000000..e4cbed11c5 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/shop_variant.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "shopVariant", -> + restrict: 'E' + replace: true + templateUrl: 'shop_variant.html' + scope: + variant: '=' diff --git a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee index a682176d77..633ecd22f4 100644 --- a/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/taxon_selector.js.coffee @@ -1,4 +1,6 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> + # Automatically builds activeSelectors for taxons + # Lots of magic here restrict: 'E' replace: true scope: @@ -8,7 +10,7 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> link: (scope, elem, attr)-> selectors_by_id = {} - selectors = ["foo"] + selectors = null # To get scoping/closure right scope.emit = -> scope.results = selectors.filter (selector)-> @@ -16,6 +18,7 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> .map (selector)-> selector.taxon.id + # Build hash of unique taxons, each of which gets an ActiveSelector scope.selectors = -> taxons = {} selectors = [] @@ -25,7 +28,11 @@ Darkswarm.directive "taxonSelector", (FilterSelectorsService)-> if object.supplied_taxons for taxon in object.supplied_taxons taxons[taxon.id] = taxon - + + # Generate a selector for each taxon. + # NOTE: THESE ARE MEMOIZED to stop new selectors from being created constantly, otherwise function always returns non-identical results + # This means the $digest cycle can never close and times out + # See http://stackoverflow.com/questions/19306452/how-to-fix-10-digest-iterations-reached-aborting-error-in-angular-1-2-fil for id, taxon of taxons if selector = selectors_by_id[id] selectors.push selector diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index 26b7ac3518..b9f59e93a8 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -1,7 +1,7 @@ window.FieldsetMixin = ($scope)-> $scope.next = (event = false)-> event.preventDefault() if event - $scope.show $scope.nextPanel + $scope.show $scope.nextPanel $scope.onTimeout = -> if $scope[$scope.name].$valid @@ -36,7 +36,6 @@ window.FieldsetMixin = ($scope)-> when "number" then "must be number" when "email" then "must be email address" - #server_errors = $scope.Order.errors[path.replace('order.', '')] - #errors.push server_errors if server_errors? - (errors.filter (error) -> error?).join ", " - + #server_errors = $scope.Order.errors[path.replace('order.', '')] + #errors.push server_errors if server_errors? + (errors.filter (error) -> error?).join ", " \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 2d91eab370..def1c9b6d0 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -19,7 +19,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> $http.post('/orders/populate', @data()).success (data, status)=> @saved() .error (response, status)=> - alert "There was an error on the server! Please refresh the page" + # TODO what shall we do here? data: => variants = {} @@ -43,6 +43,9 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> @line_items.filter (li)-> li.quantity > 0 + empty: => + @line_items_present().length == 0 + total: => @line_items_present().map (li)-> li.variant.getPrice() @@ -57,6 +60,6 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> create_line_item: (variant)-> variant.line_item = variant: variant - quantity: 0 + quantity: null max_quantity: null @line_items.push variant.line_item diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 920c01dad9..cafd4d6718 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -32,6 +32,10 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h if @ship_address_same_as_billing munged_order.ship_address_attributes = munged_order.bill_address_attributes + # If the order already has a ship and bill address (as with logged in users with + # past orders), and we don't remove id here, then this will set the wrong id for + # ship address, and Rails will error with a 404 for that address. + delete munged_order.ship_address_attributes.id if @paymentMethod()?.method_type == 'gateway' angular.extend munged_order.payments_attributes[0], { diff --git a/app/assets/javascripts/darkswarm/services/current_order.js.coffee b/app/assets/javascripts/darkswarm/services/current_order.js.coffee index a0714dc005..cd2402f0c0 100644 --- a/app/assets/javascripts/darkswarm/services/current_order.js.coffee +++ b/app/assets/javascripts/darkswarm/services/current_order.js.coffee @@ -1,5 +1,3 @@ Darkswarm.factory 'CurrentOrder', (currentOrder) -> new class CurrentOrder order: currentOrder - empty: => - @order.line_items.length == 0 diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index cc4ffbf259..9da3187f82 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -28,6 +28,8 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car for product in @products 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 registerVariantsWithCart: -> @@ -44,5 +46,6 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Car product.price = Math.min.apply(null, prices) product.hasVariants = product.variants?.length > 0 - product.primaryImage = product.images[0]?.small_url + product.primaryImage = product.images[0]?.small_url if product.images product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" + product.largeImage = product.images[0]?.large_url if product.images diff --git a/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee b/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee index 3980aa5fed..6d060179b3 100644 --- a/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee +++ b/app/assets/javascripts/darkswarm/services/shipping_methods.js.coffee @@ -4,5 +4,6 @@ Darkswarm.factory "ShippingMethods", (shippingMethods)-> shipping_methods_by_id: {} constructor: -> for method in @shipping_methods + method.price = parseFloat(method.price) @shipping_methods_by_id[method.id] = method diff --git a/app/assets/javascripts/darkswarm/services/variants.js.coffee b/app/assets/javascripts/darkswarm/services/variants.js.coffee index d313458b75..0f231ac030 100644 --- a/app/assets/javascripts/darkswarm/services/variants.js.coffee +++ b/app/assets/javascripts/darkswarm/services/variants.js.coffee @@ -7,4 +7,5 @@ Darkswarm.factory 'Variants', -> extend: (variant)-> variant.getPrice = -> variant.price * variant.line_item.quantity + variant.basePricePercentage = Math.round(variant.base_price / variant.price * 100) variant diff --git a/app/assets/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml index 0f7ffb033d..caa3bc5e65 100644 --- a/app/assets/javascripts/templates/partials/contact.html.haml +++ b/app/assets/javascripts/templates/partials/contact.html.haml @@ -1,14 +1,14 @@ -%div{bindonce: true} +%div.contact-container{bindonce: true} %div.modal-centered{"bo-if" => "enterprise.email || enterprise.website || enterprise.phone"} %p.modal-header Contact %p{"ng-if" => "enterprise.phone"} {{ enterprise.phone }} - %p{"ng-if" => "enterprise.email"} + %p.word-wrap{"ng-if" => "enterprise.email"} %a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true} %span.email {{ enterprise.email | stripUrl }} - %p{"ng-if" => "enterprise.website"} + %p.word-wrap{"ng-if" => "enterprise.website"} %a{"ng-href" => "http://{{enterprise.website | stripUrl}}", target: "_blank" } {{ enterprise.website | stripUrl }} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index 7b1fc74a03..96a4fb207e 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -1,12 +1,12 @@ .highlight .highlight-top %p.right - {{ [enterprise.address.city, enterprise.address.state] | printArray}} + {{ [enterprise.address.city, enterprise.address.state_name] | printArray}} %h3{"ng-if" => "enterprise.is_distributor"} %a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise", bindonce: true} %i.ofn-i_040-hub - {{ enterprise.name }} + %span {{ enterprise.name }} %h3{"ng-if" => "!enterprise.is_distributor"} %i.ofn-i_036-producers - {{ enterprise.name }} + %span {{ enterprise.name }} %img.hero-img{"ng-src" => "{{enterprise.promo_image}}"} diff --git a/app/assets/javascripts/templates/partials/hub_actions.html.haml b/app/assets/javascripts/templates/partials/hub_actions.html.haml index 2ed8bf3f65..54b8ee98b1 100644 --- a/app/assets/javascripts/templates/partials/hub_actions.html.haml +++ b/app/assets/javascripts/templates/partials/hub_actions.html.haml @@ -11,6 +11,6 @@ %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} {{hub.name}} - .button-address {{ hub.address.city }} , {{hub.address.state}} + .button-address {{ hub.address.city }} , {{hub.address.state_name}} %i.ofn-i_007-caret-right diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 256494bb4a..8de1039f10 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -1,10 +1,10 @@ .row.pad-top{bindonce: true} .cta-container.small-12.columns .row - .small-12.large-6.columns - %label{"active-table-hub-link" => "enterprise", change: "Change hub to", shop: "Shop now at"} - .small-12.large-6.columns.right - .right{"bo-if" => "enterprise.pickup || enterprise.delivery"} + .small-4.columns + %label{"active-table-hub-link" => "enterprise", change: "Change hub to:", shop: "Shop now at:"} + .small-8.columns.right + %label.right{"bo-if" => "enterprise.pickup || enterprise.delivery"} Delivery options: %span{"bo-if" => "enterprise.pickup"} %i.ofn-i_038-takeaway @@ -20,5 +20,5 @@ %i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"} {{enterprise.name}} - .button-address {{ enterprise.address.city }} , {{enterprise.address.state}} + .button-address {{ enterprise.address.city }} , {{enterprise.address.state_name}} %i.ofn-i_007-caret-right diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 5e7c82e0cf..0b7eba917a 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,4 +1,38 @@ -.joyride-tip-guide{"ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} - %span.joyride-nub.bottom +.joyride-tip-guide.price_breakdown{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} + %span.joyride-nub.right .joyride-content-wrapper - {{ variant.id }} + .collapsed{"ng-show" => "!expanded"} + %price-percentage{percentage: 'variant.basePricePercentage'} + %a{"ng-click" => "expanded = !expanded"} + Full price breakdown + %i.ofn-i_005-caret-down + + .expanded{"ng-show" => "expanded"} + %ul + %li.cost + .right {{ variant.base_price | currency }} + Item cost + %li{"bo-if" => "variant.fees.admin"} + .right {{ variant.fees.admin | currency }} + Admin fee + %li{"bo-if" => "variant.fees.sales"} + .right {{ variant.fees.sales | currency }} + Sales fee + %li{"bo-if" => "variant.fees.packing"} + .right {{ variant.fees.packing | currency }} + Packing fee + %li{"bo-if" => "variant.fees.transport"} + .right {{ variant.fees.transport | currency }} + Transport fee + %li{"bo-if" => "variant.fees.fundraising"} + .right {{ variant.fees.fundraising | currency }} + Fundraising fee + %li + %strong + .right = {{ variant.price | currency }} +   + + %a{"ng-click" => "expanded = !expanded"} + Price graph + %i.ofn-i_006-caret-up + diff --git a/app/assets/javascripts/templates/price_breakdown_button.html.haml b/app/assets/javascripts/templates/price_breakdown_button.html.haml new file mode 100644 index 0000000000..8a86ed2618 --- /dev/null +++ b/app/assets/javascripts/templates/price_breakdown_button.html.haml @@ -0,0 +1,2 @@ +%button.graph-button{"ng-class" => "{open: tt_isOpen}"} + %i.ofn-i-058-graph diff --git a/app/assets/javascripts/templates/price_percentage.html.haml b/app/assets/javascripts/templates/price_percentage.html.haml new file mode 100644 index 0000000000..e577d86d7a --- /dev/null +++ b/app/assets/javascripts/templates/price_percentage.html.haml @@ -0,0 +1,5 @@ +.progress + .right Fees + .meter + Item cost + diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 8b35cf79c5..82cda0a371 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,9 +1,9 @@ .row .columns.small-12.large-6 - %img.product-img{"ng-src" => "{{product.primaryImage}}", "ng-if" => "product.primaryImage"} + %img.product-img{"ng-src" => "{{product.largeImage}}", "ng-if" => "product.largeImage"} .columns.small-12.large-6.product-header %h2 - %render-svg{path: "{{product.primary_taxon.icon}}"} + / %render-svg{path: "{{product.primary_taxon.icon}}"} {{product.name}} %p {{product.description}} %ng-include{src: "'partials/close.html'"} diff --git a/app/views/shop/products/_variants.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml similarity index 72% rename from app/views/shop/products/_variants.html.haml rename to app/assets/javascripts/templates/shop_variant.html.haml index 3427d23a5f..a6db8b4e41 100644 --- a/app/views/shop/products/_variants.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -1,16 +1,15 @@ -.row.variants{bindonce: true, - "ng-repeat" => "variant in product.variants track by variant.id"} - +.variants.row .small-12.medium-4.large-4.columns.variant-name .table-cell .inline {{ variant.name_to_display }} - .bulk-buy.inline{"bo-if" => "product.group_buy"} + .bulk-buy.inline{"bo-if" => "variant.product.group_buy"} %i.ofn-i_056-bulk>< %em>< \ Bulk -# WITHOUT GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!product.group_buy"} + .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} + %input{type: :number, value: nil, min: 0, @@ -22,7 +21,7 @@ -# WITH GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "product.group_buy"} + .small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} %span.bulk-input-container %span.bulk-input %input.bulk.first{type: :number, @@ -33,7 +32,7 @@ "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input{"bo-if" => "product.group_buy"} + %span.bulk-input{"bo-if" => "variant.product.group_buy"} %input.bulk.second{type: :number, min: 0, "ng-model" => "variant.line_item.max_quantity", @@ -51,8 +50,11 @@ %i.ofn-i_009-close {{ variant.price | currency }} - / %button.graph-button{popover: "This is the popover text", "popover-title" => "The title.", "popover-animation" => "true", "popover-trigger" =>"mouseenter", "popover-placement" => "top", "tabindex" => "-1"} - / %i.ofn-i-058-graph + -# Now in a template in app/assets/javascripts/templates ! + %price-breakdown{"price-breakdown" => "_", variant: "variant", + "price-breakdown-append-to-body" => "true", + "price-breakdown-placement" => "left", + "price-breakdown-animation" => true} .small-12.medium-2.large-2.columns.total-price.text-right .table-cell diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 87983a6bc7..3281c8d116 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -140,6 +140,24 @@ table#listing_enterprise_groups { } } +#no_results { + font-weight:bold; + color: #DA5354; +} + + +#loading { + text-align: center; + img.spinner { + width: 100px; + height: 100px; + } + h1 { + margin-top: 20px; + color: gray; + } +} + .ofn_drop_down { padding: 7px 15px; border-radius: 3px; diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index c8ceaaaac2..bddc7f9e9e 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -2,59 +2,6 @@ display: block; } -div.pagination { - div.pagenav { - margin: 0px; - span.first, span.prev, span.next, span.last { - padding: 5px 0px; - display:inline-block; - } - } -} - -div.pagination_info { - text-align: right; -} - - - -div.applied_filter { - margin-bottom: 5px; - border: solid 2px #5498da; - padding: 5px 0px; - border-radius: 5px; - div.four.columns { - padding-left: 10px; - } -} - -div.option_tabs { - div.applied_filters, div.filters, div.column_toggle { - margin-bottom: 10px; - } -} - -div.option_tab_titles { - h6 { - border-radius: 3px; - border: 1px solid #cee1f4; - padding: 3px; - text-align: center; - color: darken(#cee1f4, 3); - cursor: pointer; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - } - h6.selected { - border: 1px solid #5498da; - color: #5498da; - } - margin-bottom: 10px; -} - tbody.odd { tr.product { td { background-color: white; } } tr.variant.odd { td { background-color: lighten(#eff5fc, 3); } } @@ -76,41 +23,41 @@ th.left-actions, td.left-actions { border-right: 1px solid #cee1f4 !important; } -li.column-list-item { - border-radius: 3px; - padding: 2px 20px; - margin: 2px 1px; - background-color: white; - border: 2px solid lightgray; - color: darkgray; - font-size: 100%; - cursor: default; - text-align: center; - cursor: pointer; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -li.column-list-item.selected { - border: 2px solid #5498da; - background-color: #5498da; - color: white; - font-size: 100%; -} - -ul.column-list { - list-style: none; +#update-status-message { + margin: 4px 0px; + font-weight: bold; } table#listing_products.bulk { clear: both; - td.supplier { - select { - width: 125px; + colgroup col { + &.producer { + width: 18%; + } + &.name { + width: 18%; + } + &.unit { + width: 14%; + } + &.display_as { + width: 12%; + } + &.price { + width: 10%; + } + &.on_hand { + width: 10%; + } + &.category { + width: 15%; + } + &.available_on { + width: 15%; + } + &.actions { + width: 3%; } } diff --git a/app/assets/stylesheets/admin/enterprise_relationships.css.sass b/app/assets/stylesheets/admin/relationships.css.sass similarity index 82% rename from app/assets/stylesheets/admin/enterprise_relationships.css.sass rename to app/assets/stylesheets/admin/relationships.css.sass index cffc0b0623..a5b8879125 100644 --- a/app/assets/stylesheets/admin/enterprise_relationships.css.sass +++ b/app/assets/stylesheets/admin/relationships.css.sass @@ -7,7 +7,10 @@ background-color: #fff -table#enterprise-relationships +table#enterprise-relationships, table#enterprise-roles + ul + list-style-type: none + th.actions, td.actions width: 16% .errors diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index fec2eae4a6..6fa8a72c1b 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -1,37 +1,117 @@ @import mixins +@import branding -.darkswarm - product - // Pop over +// .darkswarm +// product - // Foundation overrides - .joyride-tip-guide - // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS - background-color: #ebebeb - border: 1px solid #a5a5a5 - color: #1f1f1f - - h1, h2, h3, h4, h5, h6 - color: #1f1f1f - - .joyride-nub.bottom - border-color: #a5a5a5 !important - border-bottom-color: transparent !important - border-left-color: transparent !important - border-right-color: transparent !important - - button.graph-button - padding: 0 +ordercycle + .joyride-tip-guide + background-color: $clr-brick + .joyride-nub.right + border-color: $clr-brick !important + border-top-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + p margin: 0 - @include border-radius(0) - display: inline - background: none + font-weight: 700 + +// Pop over +// Foundation overrides +.joyride-tip-guide.price_breakdown + // JS needs to be tweaked to adjust for left alignment - this is dynamic can't rewrite in CSS + background-color: #999 + color: #1f1f1f + margin-left: -8px + @include box-shadow(0 1px 2px 0 rgba(0,0,0,0.7)) + + .joyride-content-wrapper + padding: 1.125rem 1.25rem 1.5rem + padding: 1rem + margin: 1% + width: 98% + background-color: white + + h1, h2, h3, h4, h5, h6 + color: #1f1f1f + + .joyride-nub.right + top: 38px + border-color: #999 !important + border-top-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + + .progress + background-color: #13bf85 + padding: 0 + border: none + color: white + font-size: 0.75rem + font-style: oblique + line-height: 1 + height: auto + .right + padding: 0.5rem 0.25rem 0 0 + .meter + background-color: #0b8c61 + padding: 0.5rem 0.25rem + border-right: 1px solid #539f92 + + .expanded + ul, li + list-style: none + margin: 0 + font-size: 0.875rem + li + background-color: #13bf85 + padding: 0 0.25rem + margin-bottom: 2px + color: white + li.cost + background-color: #0b8c61 + li:last-child + margin-bottom: 0.75rem + + +button.graph-button + z-index: 9999999 + border: 1px solid transparent + @include box-shadow(none) + padding: 0 + margin: 0 + @include border-radius(999rem) + display: inline + background-color: rgba(255,255,255,0.5) + padding: 5px + + &:hover, &:active, &:focus + background-color: rgba(255,255,255,1) + i.ofn-i-058-graph + color: $clr-brick-bright + + i.ofn-i-058-graph + color: #999 + margin: 0 + padding: 0 + font-size: 1rem + + @media all and (max-width: 640px) + padding: 3px + i.ofn-i-058-graph + font-size: 0.75rem + +button.graph-button.open + @include box-shadow(inset 0 1px 1px 0 rgba(0,0,0,0.35)) + border: 1px solid #999 + + &:hover, &:active, &:focus + background-color: rgba(255,255,255,1) + i.ofn-i-058-graph + color: $clr-brick-bright + + i.ofn-i-058-graph + color: $clr-brick + - i.ofn-i-058-graph - color: #999 - margin: 0 - padding: 0 - font-size: 1rem - &:hover, &:focus, &:active, &.active - color: #444 \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index 2dbbf46a45..c2aadf5a46 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -1,3 +1,5 @@ +@import branding.css.sass + .darkswarm products product @@ -25,9 +27,13 @@ .row.variants margin-left: 0 margin-right: 0 - background: url("/assets/gray_jean.png") top left repeat + background-color: #ECECEC + &:hover, &:focus, &:active + background-color: $clr-brick-light &:nth-of-type(even) - background: url("/assets/gray_jean_light.png") top left repeat + background-color: #f9f9f9 + &:hover, &:focus, &:active + background-color: $clr-brick-ultra-light // Variant name .variant-name @@ -72,7 +78,6 @@ .table-cell height: 27px - // ROW SUMMARY .row.summary margin-left: 0 @@ -100,12 +105,10 @@ h3 font-size: 1.5rem margin: 0 - a h3 - color: black - - - - + h3 a + color: #222 + &:hover, &:focus, &:active + color: black diff --git a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass index 8bc1550d54..7a69707a60 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-thumb.css.sass @@ -13,7 +13,7 @@ float: left display: block z-index: 999999 - background-color: #999 + background-color: white overflow: hidden @media all and (max-width: 768px) diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index 737915086b..68f60dde53 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -8,12 +8,13 @@ // $clr-turquoise: #097563 // $clr-turquoise-light: #cef2ec // $clr-turquoise-ultra-light: #e6faf7 -// $clr-turquoise-bright: #1d8f7c +// $clr-turquoise-bright: #1d8f7c $clr-brick: #c1122b $clr-brick-light: #f5e6e7 $clr-brick-ultra-light: #faf5f6 $clr-brick-bright: #eb4c46 +$clr-brick-med-bright: #e5a2a0 $clr-brick-light-bright: #f5c4c9 $clr-turquoise: #0b8c61 diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index e9a7f09597..a592c5dc25 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -3,10 +3,19 @@ checkout display: block + + @media all and (max-width: 640px) + &.row .row + margin-left: 0 + margin-right: 0 orderdetails .button, table width: 100% + @media all and (max-width: 640px) + form.edit_order + border: 1px solid $disabled-bright + margin-bottom: 2rem #details, #billing, #shipping, #payment border: 0 diff --git a/app/assets/stylesheets/darkswarm/home_tagline.css.sass b/app/assets/stylesheets/darkswarm/home_tagline.css.sass index ce8b275747..ed85e07d60 100644 --- a/app/assets/stylesheets/darkswarm/home_tagline.css.sass +++ b/app/assets/stylesheets/darkswarm/home_tagline.css.sass @@ -7,24 +7,19 @@ background-color: black background-image: url("/assets/home/ofn_bg_1.jpg") @include fullbg - height: 500px + height: 400px padding: 40px 0px - h1, h2, span, small, timer + h1, h2, p color: white - p - color: $clr-brick-light h1 - margin-bottom: 3rem + margin-bottom: 1em h2 font-size: 1.6875rem max-width: 610px margin: 0 auto - padding-bottom: 0.5rem a color: white &:hover, &:active, &:focus color: $clr-brick-light-bright @include textsoftpress - a.button.primary - color: white \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/hub_node.css.sass b/app/assets/stylesheets/darkswarm/hub_node.css.sass index de0b1ca58c..d0b5dcc0ef 100644 --- a/app/assets/stylesheets/darkswarm/hub_node.css.sass +++ b/app/assets/stylesheets/darkswarm/hub_node.css.sass @@ -12,7 +12,7 @@ //Hub icon styline i.ofn-i_040-hub - @include border-radius(9999em) + @include border-radius(99999rem) font-size: 1.15rem display: inline-block padding: 0.2rem diff --git a/app/assets/stylesheets/darkswarm/images.css.sass b/app/assets/stylesheets/darkswarm/images.css.sass index 39a86a93d1..ce205f0dae 100644 --- a/app/assets/stylesheets/darkswarm/images.css.sass +++ b/app/assets/stylesheets/darkswarm/images.css.sass @@ -11,9 +11,9 @@ @include box-shadow(0 1px 2px 1px rgba(0,0,0,0.25)) .hero-img - background-color: #333 + border-bottom: 1px solid $disabled-bright width: 100% - min-height: 160px + min-height: 56px height: inherit max-height: 260px overflow: hidden diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index 2050cf5063..53e8fb5768 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -1,16 +1,27 @@ // Place all the styles related to the map controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ +@import big-input + .map-container width: 100% map, .angular-google-map-container, google-map, .angular-google-map display: block height: 100% + width: 100% img // https://github.com/zurb/foundation/issues/112 max-width: none height: auto - #pac-input - padding: 4px - font-size: 2em + #pac-input + @include big-input(#888, #333, $clr-brick) + @include big-input-static + font-size: 1.5rem + background: rgba(255,255,255,0.85) + width: 50% + margin-top: 1.2rem + @media all and (max-width: 768px) + width: 80% + &:active, &:focus, &.active + background: rgba(255,255,255, 1) diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 7991969a7d..4883aa3572 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -22,6 +22,9 @@ p line-height: 2 + h3 a:hover span + border-bottom: 1px solid $clr-brick-bright + // ABOUT Enterprise @@ -41,6 +44,11 @@ max-width: 180px max-height: 180px +// CONTACT Enterprise + +.contact-container + a:hover + text-decoration: underline // FOLLOW Enterprise @@ -60,19 +68,26 @@ // CALL TO ACTION - hub click throughs .cta-container - background: url("/assets/gray_jean.png") repeat + background-color: #ececec padding-top: 0.75rem label text-transform: uppercase font-size: 0.875rem - margin-bottom: 0.5rem + margin-bottom: 0 + 5rem color: $dark-grey + label.right + color: $disabled-dark + span + text-transform: capitalize + .button.secondary background-color: #999 .button.hub margin-right: 1rem + margin-top: 0.25rem margin-bottom: 1rem padding-left: 1rem padding-right: 1rem @@ -114,4 +129,4 @@ border-bottom: 1px solid $disabled-dark margin-top: 0.75rem margin-bottom: 0.5rem - + diff --git a/app/assets/stylesheets/darkswarm/modals.css.sass b/app/assets/stylesheets/darkswarm/modals.css.sass index 182ab5bec0..0681dd73b4 100644 --- a/app/assets/stylesheets/darkswarm/modals.css.sass +++ b/app/assets/stylesheets/darkswarm/modals.css.sass @@ -4,46 +4,66 @@ dialog, .reveal-modal border: none outline: none - padding: 1rem - div - overflow: scroll - @media only screen and (min-width: 40.063em) - max-height: 580px - @media all and (max-width: 768px) - max-height: 440px - @media all and (max-width: 640px) - max-height: 400px - @media all and (max-width: 640px) - max-height: inherit - overflow: scroll + padding: 1rem + overflow-y: scroll + + // Sets up max heights based on device height + @media all and (min-height: 1025px) + max-height: 80% + + @media all and (min-height: 700px) and (max-height: 1024px) + max-height: 70% + + @media all and (min-height: 600px) and (max-height: 699px) + max-height: 60% + + @media all and (min-height: 481px) and (max-height: 599px) + max-height: 60% + + @media only screen and (max-height: 480px) and (min-width: 641px) + max-height: 60% + + @media all and (max-height: 480px) + overflow-y: scroll + .reveal-modal-bg background-color: rgba(0,0,0,0.65) -dialog .close-reveal-modal.outside, .reveal-modal .close-reveal-modal.outside - top: -2.5rem - right: -2.5rem - font-size: 2rem - color: white +dialog .close-reveal-modal, .reveal-modal .close-reveal-modal + right: 0.4rem + background-color: rgba(235,235,235,0.85) text-shadow: none - padding: 0.25rem - @include border-radius(999999) - border: 1px solid transparent + padding: 0.3rem + @include border-radius(999999rem) &:hover, &:active, &:focus - text-shadow: 0 1px 3px #333 - border: 1px solid white + background-color: rgba(235,235,235,1) + color: #333 - @media all and (max-width: 640px) - top: 0.5rem - right: 0.5rem - font-size: 2rem - color: white - text-shadow: none - padding: 0.25rem - background-color: rgba(150,150,150,0.85) - @include border-radius(999999) - border: 1px solid transparent - &:hover, &:active, &:focus - text-shadow: 0 1px 3px #333 - border: 1px solid white +// dialog .close-reveal-modal.outside, .reveal-modal .close-reveal-modal.outside +// top: -2.5rem +// right: -2.5rem +// font-size: 2rem +// color: white +// text-shadow: none +// padding: 0.25rem +// @include border-radius(999999) +// border: 1px solid transparent +// &:hover, &:active, &:focus +// text-shadow: 0 1px 3px #333 +// border: 1px solid white + +// @media all and (max-width: 640px) +// top: 0.5rem +// right: 0.5rem +// font-size: 2rem +// color: white +// text-shadow: none +// padding: 0.25rem +// background-color: rgba(150,150,150,0.85) +// @include border-radius(999999) +// border: 1px solid transparent +// &:hover, &:active, &:focus +// text-shadow: 0 1px 3px #333 +// border: 1px solid white diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 70b3997825..083defca77 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -30,6 +30,7 @@ product + @include csstrans border-bottom: 1px solid #e5e5e5 border-top: 1px solid #e5e5e5 padding-bottom: 1px @@ -38,6 +39,10 @@ display: block color: #444 + &:hover, &:focus, &:active + border-bottom: 1px solid $clr-brick-med-bright + border-top: 1px solid $clr-brick-med-bright + // BULK .bulk-buy font-size: 0.875rem @@ -55,6 +60,8 @@ i font-size: 0.75em padding-right: 0.9375rem + @media all and (max-width: 640px) + padding-right: 0.25rem i.ofn-i_056-bulk, i.ofn-i_036-producers font-size: 1rem diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index 745dceb70f..72856eba72 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -12,14 +12,21 @@ display: block right: 10px top: 55px - width: 400px + width: 480px + @media screen and (max-width: 640px) + width: 96% .joyride-nub right: 22px !important left: auto + ul, li + list-style: none + margin-left: 0 + li float: none + .row .columns padding-left: 0.25rem padding-right: 0.25rem diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index 254a277055..b49ce867c1 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -34,6 +34,10 @@ small, .small margin-bottom: 0.5rem &, & * font-size: 0.875rem + +.word-wrap + word-wrap: break-word + .light color: #999 display: inline diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass index bd740ec614..a4d70392bb 100644 --- a/app/assets/stylesheets/darkswarm/ui.css.sass +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -46,6 +46,7 @@ .button, button @include border-radius(0.5em) + outline: none // Turn off blue highlight on chrome .button.primary, button.primary font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif @@ -57,10 +58,10 @@ text-shadow: 0 1px 0 $clr-brick button.success, .button.success - background: $clr-turquoise + background: #0096ad .button.success:hover, .button.success:active, .button.success:focus, button.success:hover, button.success:active, button.success:focus - background: $clr-turquoise-bright + background: #14b6cc // Responsive @media screen and (min-width: 768px) diff --git a/app/controllers/admin/enterprise_relationships_controller.rb b/app/controllers/admin/enterprise_relationships_controller.rb index 212bf3849d..7ef435d7ed 100644 --- a/app/controllers/admin/enterprise_relationships_controller.rb +++ b/app/controllers/admin/enterprise_relationships_controller.rb @@ -10,7 +10,7 @@ module Admin @enterprise_relationship = EnterpriseRelationship.new params[:enterprise_relationship] if @enterprise_relationship.save - render partial: "admin/json/enterprise_relationship", locals: {enterprise_relationship: @enterprise_relationship} + render text: Api::Admin::EnterpriseRelationshipSerializer.new(@enterprise_relationship).to_json else render status: 400, json: {errors: @enterprise_relationship.errors.full_messages.join(', ')} end diff --git a/app/controllers/admin/enterprise_roles_controller.rb b/app/controllers/admin/enterprise_roles_controller.rb new file mode 100644 index 0000000000..8ffc2bad01 --- /dev/null +++ b/app/controllers/admin/enterprise_roles_controller.rb @@ -0,0 +1,26 @@ +module Admin + class EnterpriseRolesController < ResourceController + def index + @enterprise_roles = EnterpriseRole.by_user_email + @users = Spree::User.order('spree_users.email') + @my_enterprises = @all_enterprises = Enterprise.by_name + end + + def create + @enterprise_role = EnterpriseRole.new params[:enterprise_role] + + if @enterprise_role.save + render text: Api::Admin::EnterpriseRoleSerializer.new(@enterprise_role).to_json + + else + render status: 400, json: {errors: @enterprise_role.errors.full_messages.join(', ')} + end + end + + def destroy + @enterprise_role = EnterpriseRole.find params[:id] + @enterprise_role.destroy + render nothing: true + end + end +end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 53e58d110b..85fe6831fb 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -6,6 +6,11 @@ module Admin create.after :grant_management helper 'spree/products' + include OrderCyclesHelper + + def for_order_cycle + @collection = order_cycle_permitted_enterprises + end def bulk_update @@ -53,7 +58,7 @@ module Admin end def collection_actions - [:index, :bulk_update] + [:index, :for_order_cycle, :bulk_update] end def load_methods_and_fees diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 566c660112..63daf918de 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,7 +1,10 @@ +require 'open_food_network/permissions' require 'open_food_network/order_cycle_form_applicator' module Admin class OrderCyclesController < ResourceController + include OrderCyclesHelper + before_filter :load_order_cycle_set, :only => :index def show @@ -23,7 +26,7 @@ module Admin respond_to do |format| if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been created.' format.html { redirect_to admin_order_cycles_path } @@ -40,7 +43,7 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 11f707d9e6..7941deb9ee 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,25 +1,62 @@ -Spree::Admin::PaymentMethodsController.class_eval do - # Only show payment methods that user has access to and sort by distributor name - # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked - def collection - return parent.send(controller_name) if parent_data.present? - collection = if model_class.respond_to?(:accessible_by) && - !current_ability.has_block?(params[:action], model_class) +module Spree + module Admin + PaymentMethodsController.class_eval do + skip_before_filter :load_resource, only: [:show_provider_preferences] + before_filter :load_hubs, only: [:new, :edit, :update] + create.before :load_hubs - model_class.accessible_by(current_ability, action) + # Only show payment methods that user has access to and sort by distributor name + # ! Redundant code copied from Spree::Admin::ResourceController with modifications marked + def collection + return parent.send(controller_name) if parent_data.present? + collection = if model_class.respond_to?(:accessible_by) && + !current_ability.has_block?(params[:action], model_class) - else - model_class.scoped - end + model_class.accessible_by(current_ability, action) - collection = collection.managed_by(spree_current_user).by_name # This line added + else + model_class.scoped + end - # This block added - if params.key? :enterprise_id - distributor = Enterprise.find params[:enterprise_id] - collection = collection.for_distributor(distributor) + collection = collection.managed_by(spree_current_user).by_name # This line added + + # This block added + if params.key? :enterprise_id + distributor = Enterprise.find params[:enterprise_id] + collection = collection.for_distributor(distributor) + end + + collection + end + + def show_provider_preferences + if params[:pm_id].present? + @payment_method = PaymentMethod.find(params[:pm_id]) + authorize! :show_provider_preferences, @payment_method + payment_method_type = params[:provider_type] + if @payment_method['type'].to_s != payment_method_type + @payment_method.update_column(:type, payment_method_type) + @payment_method = PaymentMethod.find(params[:pm_id]) + end + else + @payment_method = params[:provider_type].constantize.new() + end + render partial: 'provider_settings' + end + + private + + def load_data + if spree_current_user.admin? || Rails.env.test? + @providers = Gateway.providers.sort{|p1, p2| p1.name <=> p2.name } + else + @providers = Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort{|p1, p2| p1.name <=> p2.name } + end + end + + def load_hubs + @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } + end end - - collection end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 4f0c592284..a5b38126e5 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -1,5 +1,5 @@ Spree::Admin::ProductsController.class_eval do - before_filter :load_spree_api_key, :only => :bulk_edit + before_filter :load_bpe_data, :only => :bulk_edit alias_method :location_after_save_original, :location_after_save @@ -30,10 +30,17 @@ Spree::Admin::ProductsController.class_eval do "#{string}q[#{filter[:property][:db_column]}_#{filter[:predicate][:predicate]}]=#{filter[:value]};" end + # Ensure we're authorised to update all products + product_set.collection.each { |p| authorize! :update, p } + if product_set.save - redirect_to "/api/products/managed?template=bulk_index;page=1;per_page=500;#{bulk_index_query}" + redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query}" else - render :nothing => true, :status => 418 + if product_set.errors.present? + render json: { errors: product_set.errors }, status: 400 + else + render :nothing => true, :status => 500 + end end end @@ -78,8 +85,10 @@ Spree::Admin::ProductsController.class_eval do private - def load_spree_api_key + def load_bpe_data current_user.generate_spree_api_key! unless spree_current_user.spree_api_key @spree_api_key = spree_current_user.spree_api_key + @producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name + @taxons = Spree::Taxon.order(:name) end end diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 377f1dbedf..0abfd783b5 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -4,12 +4,9 @@ require 'open_food_network/products_and_inventory_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' -require 'open_food_network/model_class_from_controller_name' Spree::Admin::ReportsController.class_eval do - include OpenFoodNetwork::ModelClassFromControllerName - - # Fetches user's distributors, suppliers and order_cycles + # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory] # Render a partial for orders and fulfillment description @@ -365,9 +362,9 @@ Spree::Admin::ReportsController.class_eval do lis end.flatten #payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments - + # -- Prepare form options - my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) + my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) # My distributors and any distributors distributing products I supply @@ -386,12 +383,13 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + header = ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| total_units(line_items) }, proc { |line_items| line_items.first.variant.price }, proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, proc { |line_items| "" }, @@ -596,10 +594,10 @@ Spree::Admin::ReportsController.class_eval do def load_data my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) - distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) - @distributors = my_distributors | distributors_of_my_products + distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) + @distributors = my_distributors | distributors_of_my_products suppliers_of_products_I_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq - @suppliers = my_suppliers | suppliers_of_products_I_distribute + @suppliers = my_suppliers | suppliers_of_products_I_distribute @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') end @@ -617,4 +615,13 @@ Spree::Admin::ReportsController.class_eval do end reports end + + def total_units(line_items) + return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? + total_units = line_items.sum do |li| + scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) + li.quantity * li.variant.unit_value / scale_factor + end + total_units.round(3) + end end diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 77c1aa6632..65765e03ca 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -1,3 +1,5 @@ +require 'open_food_network/permissions' + Spree::Api::ProductsController.class_eval do def managed authorize! :admin, Spree::Product @@ -7,6 +9,14 @@ Spree::Api::ProductsController.class_eval do respond_with(@products, default_template: :index) end + def bulk_products + @products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products. + merge(product_scope). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + + render text: { products: ActiveModel::ArraySerializer.new(@products, each_serializer: Spree::Api::ProductSerializer), pages: @products.num_pages }.to_json + end def soft_delete authorize! :delete, Spree::Product diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 4bbff9bf79..9ce62b2719 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -4,6 +4,19 @@ module Admin admin_inject_json_ams "admin.enterprises", "enterprise", @enterprise, Api::Admin::EnterpriseSerializer end + def admin_inject_enterprises + admin_inject_json_ams_array("ofn.admin", "my_enterprises", @my_enterprises, Api::Admin::EnterpriseSerializer) + + admin_inject_json_ams_array("ofn.admin", "all_enterprises", @all_enterprises, Api::Admin::EnterpriseSerializer) + end + + def admin_inject_enterprise_relationships + admin_inject_json_ams_array "ofn.admin", "enterprise_relationships", @enterprise_relationships, Api::Admin::EnterpriseRelationshipSerializer + end + + def admin_inject_enterprise_roles + admin_inject_json_ams_array "ofn.admin", "enterpriseRoles", @enterprise_roles, Api::Admin::EnterpriseRoleSerializer + end + def admin_inject_payment_methods admin_inject_json_ams_array "admin.payment_methods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer end @@ -12,6 +25,21 @@ module Admin admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end + def admin_inject_producers + admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer + end + + def admin_inject_taxons + admin_inject_json_ams_array "ofn.admin", "taxons", @taxons, Api::Admin::TaxonSerializer + end + + def admin_inject_users + admin_inject_json_ams_array "ofn.admin", "users", @users, Api::Admin::UserSerializer + end + + + + def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} @@ -22,4 +50,4 @@ module Admin render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} end end -end \ No newline at end of file +end diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 5faccb781d..6441cd619e 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -1,18 +1,31 @@ module CheckoutHelper def checkout_adjustments_for_summary(order, opts={}) adjustments = order.adjustments.eligible + exclude = opts[:exclude] || {} # Remove empty tax adjustments and (optionally) shipping fees adjustments.reject! { |a| a.originator_type == 'Spree::TaxRate' && a.amount == 0 } - adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if opts[:exclude_shipping] + adjustments.reject! { |a| a.originator_type == 'Spree::ShippingMethod' } if exclude.include? :shipping enterprise_fee_adjustments = adjustments.select { |a| a.originator_type == 'EnterpriseFee' } adjustments.reject! { |a| a.originator_type == 'EnterpriseFee' } - adjustments << Spree::Adjustment.new(label: 'Distribution', amount: enterprise_fee_adjustments.sum(&:amount)) + unless exclude.include? :distribution + adjustments << Spree::Adjustment.new(label: 'Distribution', amount: enterprise_fee_adjustments.sum(&:amount)) + end adjustments end + def checkout_adjustments_total(order) + adjustments = checkout_adjustments_for_summary(order, exclude: [:shipping]) + adjustments.sum &:display_amount + end + + def checkout_cart_total_with_adjustments(order) + order.display_item_total.money.to_f + checkout_adjustments_total(order).money.to_f + end + + def validated_input(name, path, args = {}) attributes = { required: true, diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 92cd968e0b..dfd957a143 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,8 +3,20 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end + def order_cycle_permitted_enterprises + OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises + end + + def order_cycle_producer_enterprises + order_cycle_permitted_enterprises.is_primary_producer.by_name + end + def coordinating_enterprises - Enterprise.is_distributor.managed_by(spree_current_user).order('name') + order_cycle_hub_enterprises + end + + def order_cycle_hub_enterprises + order_cycle_permitted_enterprises.is_distributor.by_name end def order_cycle_local_remote_class(distributor, order_cycle) diff --git a/app/helpers/spree/admin/navigation_helper_decorator.rb b/app/helpers/spree/admin/navigation_helper_decorator.rb new file mode 100644 index 0000000000..024f467154 --- /dev/null +++ b/app/helpers/spree/admin/navigation_helper_decorator.rb @@ -0,0 +1,16 @@ +module Spree + module Admin + module NavigationHelper + # Make it so that the Reports admin tab can be enabled/disabled through the cancan + # :report resource, since it does not have a corresponding resource class (unlike + # eg. Spree::Product). + def klass_for_with_sym_fallback(name) + klass = klass_for_without_sym_fallback(name) + klass ||= name.singularize.to_sym + klass = :overview if klass == :dashboard + klass + end + alias_method_chain :klass_for, :sym_fallback + end + end +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 65a30fe696..004923097e 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -1,6 +1,9 @@ class Enterprise < ActiveRecord::Base + TYPES = %w(full single profile) ENTERPRISE_SEARCH_RADIUS = 100 + self.inheritance_column = nil + acts_as_gmappable :process_geocoding => false has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' @@ -40,9 +43,9 @@ class Enterprise < ActiveRecord::Base supports_s3 :promo_image - validates_presence_of :name - validates_presence_of :address - validates_associated :address + validates :name, presence: true + validates :type, presence: true, inclusion: {in: TYPES} + validates :address, presence: true, associated: true before_validation :set_unused_address_fields after_validation :geocode_address diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index f41ad76c96..6270f77b7a 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -10,7 +10,7 @@ class EnterpriseFee < ActiveRecord::Base attr_accessible :enterprise_id, :fee_type, :name, :calculator_type - FEE_TYPES = %w(packing transport admin sales) + FEE_TYPES = %w(packing transport admin sales fundraising) PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate'] diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 4db650f2e7..dba99cc7b3 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -1,6 +1,7 @@ class EnterpriseRelationship < ActiveRecord::Base belongs_to :parent, class_name: 'Enterprise', touch: true belongs_to :child, class_name: 'Enterprise', touch: true + has_many :permissions, class_name: 'EnterpriseRelationshipPermission' validates_presence_of :parent_id, :child_id validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established." @@ -8,9 +9,22 @@ class EnterpriseRelationship < ActiveRecord::Base scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') - scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name') scope :involving_enterprises, ->(enterprises) { where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises) } + + scope :permitting, ->(enterprises) { where('child_id IN (?)', enterprises) } + + scope :with_permission, ->(permission) { + joins(:permissions). + where('enterprise_relationship_permissions.name = ?', permission) + } + + scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') + + + def permissions_list=(perms) + perms.andand.each { |name| permissions.build name: name } + end end diff --git a/app/models/enterprise_relationship_permission.rb b/app/models/enterprise_relationship_permission.rb new file mode 100644 index 0000000000..0833c1386e --- /dev/null +++ b/app/models/enterprise_relationship_permission.rb @@ -0,0 +1,3 @@ +class EnterpriseRelationshipPermission < ActiveRecord::Base + default_scope order('name') +end diff --git a/app/models/enterprise_role.rb b/app/models/enterprise_role.rb index 94b926f84f..e3fb116146 100644 --- a/app/models/enterprise_role.rb +++ b/app/models/enterprise_role.rb @@ -1,4 +1,9 @@ class EnterpriseRole < ActiveRecord::Base belongs_to :user, :class_name => Spree.user_class belongs_to :enterprise + + validates_presence_of :user_id, :enterprise_id + validates_uniqueness_of :enterprise_id, scope: :user_id, message: "^That role is already present." + + scope :by_user_email, joins(:user).order('spree_users.email ASC') end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index f2ad8fee3e..d5a1371876 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -1,5 +1,3 @@ -require 'open_food_network/enterprise_fee_applicator' - class OrderCycle < ActiveRecord::Base belongs_to :coordinator, :class_name => 'Enterprise' has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees' @@ -165,77 +163,17 @@ class OrderCycle < ActiveRecord::Base exchange_for_distributor(distributor).andand.pickup_instructions end - - # -- Fees - - # TODO: The boundary of this class is ill-defined here. OrderCycle should not know about - # EnterpriseFeeApplicator. Clients should be able to query it for relevant EnterpriseFees. - # This logic would fit better in another service object. - - def fees_for(variant, distributor) - per_item_enterprise_fee_applicators_for(variant, distributor).sum do |applicator| - # Spree's Calculator interface accepts Orders or LineItems, - # so we meet that interface with a struct. - # Amount is faked, this is a method on LineItem - line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) - end + def exchanges_carrying(variant, distributor) + exchanges.supplying_to(distributor).with_variant(variant) end - def create_line_item_adjustments_for(line_item) - variant = line_item.variant - distributor = line_item.order.distributor - - per_item_enterprise_fee_applicators_for(variant, distributor).each do |applicator| - applicator.create_line_item_adjustment(line_item) - end - end - - def create_order_adjustments_for(order) - per_order_enterprise_fee_applicators_for(order).each do |applicator| - applicator.create_order_adjustment(order) - end + def exchanges_supplying(order) + exchanges.supplying_to(order.distributor).with_any_variant(order.variants) end private - # -- Fees - def per_item_enterprise_fee_applicators_for(variant, distributor) - fees = [] - - exchanges_carrying(variant, distributor).each do |exchange| - exchange.enterprise_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) - end - end - - coordinator_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') - end - - fees - end - - def per_order_enterprise_fee_applicators_for(order) - fees = [] - - exchanges_supplying(order).each do |exchange| - exchange.enterprise_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) - end - end - - coordinator_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') - end - - fees - end - - - # -- Misc - # If a product without variants is added to an order cycle, and then some variants are added # to that product, then the master variant is still part of the order cycle, but customers # should not be able to purchase it. @@ -246,12 +184,4 @@ class OrderCycle < ActiveRecord::Base distributed_variants.include?(product.master) && (product.variants & distributed_variants).empty? end - - def exchanges_carrying(variant, distributor) - exchanges.supplying_to(distributor).with_variant(variant) - end - - def exchanges_supplying(order) - exchanges.supplying_to(order.distributor).with_any_variant(order.variants) - end end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index e6d93f25aa..9f9c96cb15 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -2,85 +2,111 @@ class AbilityDecorator include CanCan::Ability def initialize(user) - if user.enterprises.count > 0 + add_enterprise_management_abilities user if can_manage_enterprises? user + add_product_management_abilities user if can_manage_products? user + add_relationship_management_abilities user if can_manage_relationships? user + end - # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) - # when searching for variants to add to the order - can [:create, :search, :bulk_update], nil - can [:admin, :index], :overview + def can_manage_enterprises?(user) + user.enterprises.present? + end - # Enterprise User can only access products that they are a supplier for - can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| - user.enterprises.include? product.supplier - end - can [:create], Spree::Variant - can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| - user.enterprises.include? variant.product.supplier - end + def can_manage_products?(user) + ( user.enterprises.map(&:type) & %w(single full) ).any? + end - can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty - can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image - can [:admin, :index, :read, :search], Spree::Taxon - can [:admin, :index, :read, :create, :edit], Spree::Classification + def can_manage_relationships?(user) + can_manage_enterprises? user + end - # Enterprise User can only access orders that they are a distributor for - can [:index, :create], Spree::Order - can [:read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| - # We allow editing orders with a nil distributor as this state occurs - # during the order creation process from the admin backend - order.distributor.nil? || user.enterprises.include?(order.distributor) - end - can [:admin], Spree::Order if user.admin? || user.enterprises.any?{ |e| e.is_distributor? } - can [:admin, :create], Spree::LineItem - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment - can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization + def add_enterprise_management_abilities(user) + # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) + # when searching for variants to add to the order + can [:create, :search, :bulk_update], nil - # Enterprise User can only access payment methods for their distributors - can [:index, :create], Spree::PaymentMethod - can [:admin, :read, :update, :fire, :resend, :destroy], Spree::PaymentMethod do |payment_method| - (user.enterprises & payment_method.distributors).any? - end + can [:admin, :index], :overview - can [:index, :create], Spree::ShippingMethod - can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| - (user.enterprises & shipping_method.distributors).any? - end + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty - can [:admin, :index, :create], EnterpriseRelationship - can [:destroy], EnterpriseRelationship do |enterprise_relationship| - user.enterprises.include? enterprise_relationship.parent - end + can [:admin, :index, :create], Enterprise + can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| + user.enterprises.include? enterprise + end + end - can [:create], OrderCycle - can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| - user.enterprises.include? order_cycle.coordinator - end - can [:index, :create], EnterpriseFee - can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| - user.enterprises.include? enterprise_fee.enterprise - end + def add_product_management_abilities(user) + # Enterprise User can only access products that they are a supplier for + can [:create], Spree::Product + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| + OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier + end - can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant - can [:admin, :index, :read, :create, :edit, :update], Exchange - can [:admin, :index, :read, :create, :edit, :update], ExchangeFee + can [:create], Spree::Variant + can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| + OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier + end - can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty + can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image - can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update], Enterprise do |enterprise| - user.enterprises.include? enterprise - end + can [:admin, :index, :read, :search], Spree::Taxon + can [:admin, :index, :read, :create, :edit], Spree::Classification - # Enterprise User can access reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + # Enterprise User can only access orders that they are a distributor for + can [:index, :create], Spree::Order + can [:read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| + # We allow editing orders with a nil distributor as this state occurs + # during the order creation process from the admin backend + order.distributor.nil? || user.enterprises.include?(order.distributor) + end + can [:admin], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor?) + can [:admin, :create], Spree::LineItem + + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment + can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization + + can [:create], OrderCycle + can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| + user.enterprises.include? order_cycle.coordinator + end + can [:for_order_cycle], Enterprise + + can [:index, :create], EnterpriseFee + can [:admin, :read, :edit, :bulk_update, :destroy], EnterpriseFee do |enterprise_fee| + user.enterprises.include? enterprise_fee.enterprise + end + + can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant + can [:admin, :index, :read, :create, :edit, :update], Exchange + can [:admin, :index, :read, :create, :edit, :update], ExchangeFee + + # Enterprise user can only access payment and shipping methods for their distributors + can [:index, :create], Spree::PaymentMethod + can [:admin, :read, :update, :fire, :resend, :destroy, :show_provider_preferences], Spree::PaymentMethod do |payment_method| + (user.enterprises & payment_method.distributors).any? + end + + can [:index, :create], Spree::ShippingMethod + can [:admin, :read, :update, :destroy], Spree::ShippingMethod do |shipping_method| + (user.enterprises & shipping_method.distributors).any? + end + + # Reports page + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report + end + + + def add_relationship_management_abilities(user) + can [:admin, :index, :create], EnterpriseRelationship + can [:destroy], EnterpriseRelationship do |enterprise_relationship| + user.enterprises.include? enterprise_relationship.parent end end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 4f2be99fa8..fcc6054f3b 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -1,5 +1,6 @@ -require 'open_food_network/feature_toggle' +require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/distribution_change_validator' +require 'open_food_network/feature_toggle' ActiveSupport::Notifications.subscribe('spree.order.contents_changed') do |name, start, finish, id, payload| payload[:order].reload.update_distribution_charge! @@ -133,7 +134,7 @@ Spree::Order.class_eval do line_items.each do |line_item| if provided_by_order_cycle? line_item - order_cycle.create_line_item_adjustments_for line_item + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item else pd = product_distribution_for line_item @@ -141,7 +142,9 @@ Spree::Order.class_eval do end end - order_cycle.create_order_adjustments_for self if order_cycle + if order_cycle + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + end end def set_variant_attributes(variant, attributes) diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index ddbc0251a9..01f6e2f0e8 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -4,6 +4,8 @@ Spree::PaymentMethod.class_eval do attr_accessible :distributor_ids + validates :distributors, presence: { message: "^At least one hub must be selected" } + # -- Scopes scope :managed_by, lambda { |user| if user.has_spree_role?('admin') @@ -30,4 +32,19 @@ end # Ensure that all derived classes also allow distributor_ids Spree::Gateway.providers.each do |p| p.attr_accessible :distributor_ids + p.instance_eval do + def clean_name + case name + when "Spree::PaymentMethod::Check" + "Cash/EFT/etc. (payments for which automatic validation is not required)" + when "Spree::Gateway::Migs" + "MasterCard Internet Gateway Service (MIGS)" + when "Spree::Gateway::PayPalExpress" + "PayPal Express" + else + i = name.rindex('::') + 2 + name[i..-1] + end + end + end end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 4faa54a06d..d344e19935 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -17,6 +17,7 @@ Spree::Product.class_eval do attr_accessible :supplier_id, :primary_taxon_id, :distributor_ids, :product_distributions_attributes, :group_buy, :group_buy_unit_size attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value, :unit_description, :notes, :images_attributes, :display_as + validates_associated :master, message: "^Price and On Hand must be valid" validates_presence_of :supplier validates :primary_taxon, presence: { message: "^Product Category can't be blank" } diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 63ae5e0f3a..29d2ed89c1 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,3 +1,4 @@ +require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/option_value_namer' Spree::Variant.class_eval do @@ -8,14 +9,14 @@ Spree::Variant.class_eval do accepts_nested_attributes_for :images validates_presence_of :unit_value, - if: -> v { %w(weight volume).include? v.product.variant_unit }, + if: -> v { %w(weight volume).include? v.product.andand.variant_unit }, unless: :is_master validates_presence_of :unit_description, - if: -> v { v.product.variant_unit.present? && v.unit_value.nil? }, + if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }, unless: :is_master - before_validation :update_weight_from_unit_value + before_validation :update_weight_from_unit_value, if: -> v { v.product.present? } after_save :update_units scope :with_order_cycles_inner, joins(exchanges: :order_cycle) @@ -44,7 +45,11 @@ Spree::Variant.class_eval do end def fees_for(distributor, order_cycle) - order_cycle.fees_for(self, distributor) + OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self + end + + def fees_by_type_for(distributor, order_cycle) + OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self end diff --git a/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface deleted file mode 100644 index 0ed490a94a..0000000000 --- a/app/overrides/spree/admin/payment_methods/_form/add_distributors.html.haml.deface +++ /dev/null @@ -1,8 +0,0 @@ -/ insert_before '[data-hook="environment"]' - -= f.field_container :distributors do - = f.label :distributors - %br - - distributors = Enterprise.is_distributor.managed_by(spree_current_user) | f.object.distributors - = f.collection_select(:distributor_ids, distributors, :id, :name, {include_blank: false}, {class: "select2 fullwidth", multiple: true}) - = f.error_message_on :distributors diff --git a/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface b/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface new file mode 100644 index 0000000000..613ffe010b --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/_form/remove_clearing_div.deface @@ -0,0 +1 @@ +remove "div.clear" \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface new file mode 100644 index 0000000000..3b588a98c1 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -0,0 +1,36 @@ +/ replace "div[data-hook='admin_payment_method_form_fields']" + +%div.alpha.eleven.columns + .row + .alpha.three.columns + = label_tag nil, t(:name) + .omega.eight.columns + = text_field :payment_method, :name, :class => 'fullwidth' + .row + .alpha.three.columns + = label_tag nil, t(:description) + .omega.eight.columns + = text_area :payment_method, :description, {:cols => 60, :rows => 6, :class => 'fullwidth'} + - if spree_current_user.admin? + .row + .alpha.three.columns + = label_tag nil, t(:environment) + .omega.eight.columns + = collection_select(:payment_method, :environment, Rails.configuration.database_configuration.keys.sort, :to_s, :titleize, {}, {:id => 'gtwy-env', :class => 'select2 fullwidth'}) + .row + .alpha.three.columns + = label_tag nil, t(:display) + .omega.eight.columns + = select(:payment_method, :display_on, Spree::PaymentMethod::DISPLAY.collect { |display| [t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) + .row + .alpha.three.columns + = label_tag nil, t(:active) + .two.columns + = radio_button :payment_method, :active, true +   + = label_tag nil, t(:say_yes) + .omega.six.columns + = radio_button :payment_method, :active, false +   + = label_tag nil, t(:say_no) + = render 'providers' \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface new file mode 100644 index 0000000000..26ee2888bd --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/edit/add_hubs_sidebar.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" + +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :payment_method } +.clear \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface b/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface new file mode 100644 index 0000000000..cc3fbcdee1 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/edit/remove_configuration_sidebar.deface @@ -0,0 +1 @@ +remove "code[erb-loud]:contains(\"render :partial => 'spree/admin/shared/configuration_menu'\")" \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface new file mode 100644 index 0000000000..26ee2888bd --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/new/add_hubs_sidebar.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" + +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :payment_method } +.clear \ No newline at end of file diff --git a/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface b/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface new file mode 100644 index 0000000000..cc3fbcdee1 --- /dev/null +++ b/app/overrides/spree/admin/payment_methods/new/remove_configuration_sidebar.deface @@ -0,0 +1 @@ +remove "code[erb-loud]:contains(\"render :partial => 'spree/admin/shared/configuration_menu'\")" \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface index 5d0fce3841..190cff1a17 100644 --- a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,6 @@ / replace "div[data-hook='admin_shipping_method_form_fields']" -.alpha.twelve.columns{"data-hook" => "admin_shipping_method_form_fields"} +.alpha.eleven.columns{"data-hook" => "admin_shipping_method_form_fields"} .row .alpha.three.columns = f.label :name, t(:name) @@ -41,7 +41,7 @@ = f.radio_button :require_ship_address, true   = f.label :delivery, t(:delivery) - .six.columns + .omega.six.columns = f.radio_button :require_ship_address, false   = f.label :pick_up, t(:pick_up) diff --git a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface index 74fc957e71..057013c5a6 100644 --- a/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/edit/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :shipping_method } \ No newline at end of file diff --git a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface index 74fc957e71..057013c5a6 100644 --- a/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/new/add_hubs_sidebar.html.haml.deface @@ -1,3 +1,4 @@ / insert_after "code[erb-loud]:contains(\"render :partial => 'form', :locals => { :f => f }\")" -= render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { :f => f } \ No newline at end of file +.one.column   += render :partial => 'spree/admin/shared/hubs_sidebar', :locals => { f: f, klass: :shipping_method } \ No newline at end of file diff --git a/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface b/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface new file mode 100644 index 0000000000..f99a1ebff4 --- /dev/null +++ b/app/overrides/spree/admin/users/index/add_roles_link.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_before "table#listing_users" + += render 'admin/shared/users_sub_menu' \ No newline at end of file diff --git a/app/serializers/api/address_serializer.rb b/app/serializers/api/address_serializer.rb index 18483da312..66e4267e1f 100644 --- a/app/serializers/api/address_serializer.rb +++ b/app/serializers/api/address_serializer.rb @@ -1,10 +1,12 @@ class Api::AddressSerializer < ActiveModel::Serializer - cached - delegate :cache_key, to: :object + #cached + #delegate :cache_key, to: :object - attributes :id, :zipcode, :city, :state, :state_id + attributes :id, :zipcode, :city, :state_name, :state_id, + :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, + :zipcode - def state + def state_name object.state.andand.abbr end end diff --git a/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb b/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb new file mode 100644 index 0000000000..80ce487738 --- /dev/null +++ b/app/serializers/api/admin/enterprise_relationship_permission_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::EnterpriseRelationshipPermissionSerializer < ActiveModel::Serializer + attributes :id, :name +end diff --git a/app/serializers/api/admin/enterprise_relationship_serializer.rb b/app/serializers/api/admin/enterprise_relationship_serializer.rb new file mode 100644 index 0000000000..5030620384 --- /dev/null +++ b/app/serializers/api/admin/enterprise_relationship_serializer.rb @@ -0,0 +1,13 @@ +class Api::Admin::EnterpriseRelationshipSerializer < ActiveModel::Serializer + attributes :id, :parent_id, :parent_name, :child_id, :child_name + + has_many :permissions + + def parent_name + object.parent.name + end + + def child_name + object.child.name + end +end diff --git a/app/serializers/api/admin/enterprise_role_serializer.rb b/app/serializers/api/admin/enterprise_role_serializer.rb new file mode 100644 index 0000000000..3b5f889f8c --- /dev/null +++ b/app/serializers/api/admin/enterprise_role_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::EnterpriseRoleSerializer < ActiveModel::Serializer + attributes :id, :user_id, :enterprise_id, :user_email, :enterprise_name + + def user_email + object.user.email + end + + def enterprise_name + object.enterprise.name + end +end diff --git a/app/serializers/api/admin/taxon_serializer.rb b/app/serializers/api/admin/taxon_serializer.rb new file mode 100644 index 0000000000..61766c87cf --- /dev/null +++ b/app/serializers/api/admin/taxon_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::TaxonSerializer < ActiveModel::Serializer + attributes :id, :name, :pretty_name +end \ No newline at end of file diff --git a/app/serializers/api/admin/user_serializer.rb b/app/serializers/api/admin/user_serializer.rb new file mode 100644 index 0000000000..501cd75674 --- /dev/null +++ b/app/serializers/api/admin/user_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::UserSerializer < ActiveModel::Serializer + attributes :id, :email +end diff --git a/app/serializers/api/current_order_serializer.rb b/app/serializers/api/current_order_serializer.rb index 6275710789..9f939b09c1 100644 --- a/app/serializers/api/current_order_serializer.rb +++ b/app/serializers/api/current_order_serializer.rb @@ -10,4 +10,8 @@ class Api::CurrentOrderSerializer < ActiveModel::Serializer def payment_method_id object.payments.first.andand.payment_method_id end + + def display_total + object.display_total.money.to_f + end end diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index c5dbe05634..e03f1e0ec8 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,8 +1,16 @@ class Api::VariantSerializer < ActiveModel::Serializer attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, - :on_demand, :price + :on_demand, :price, :fees, :base_price def price object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) end + + def base_price + object.price + end + + def fees + object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) + end end diff --git a/app/serializers/spree/api/product_serializer.rb b/app/serializers/spree/api/product_serializer.rb new file mode 100644 index 0000000000..aa8919f8d6 --- /dev/null +++ b/app/serializers/spree/api/product_serializer.rb @@ -0,0 +1,26 @@ +class Spree::Api::ProductSerializer < ActiveModel::Serializer + attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand + + attributes :on_hand, :price, :available_on, :permalink_live + + has_one :supplier, key: :producer, embed: :id + has_one :primary_taxon, key: :category, embed: :id + has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids + has_one :master, serializer: Spree::Api::VariantSerializer + + def on_hand + object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand" + end + + def price + object.price.nil? ? '0.0' : object.price + end + + def available_on + object.available_on.blank? ? "" : object.available_on.strftime("%F %T") + end + + def permalink_live + object.permalink + end +end \ No newline at end of file diff --git a/app/serializers/spree/api/variant_serializer.rb b/app/serializers/spree/api/variant_serializer.rb new file mode 100644 index 0000000000..f594a540ef --- /dev/null +++ b/app/serializers/spree/api/variant_serializer.rb @@ -0,0 +1,12 @@ +class Spree::Api::VariantSerializer < ActiveModel::Serializer + attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name + attributes :on_hand, :price + + def on_hand + object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" ) + end + + def price + object.price.nil? ? 0.to_f : object.price + end +end \ No newline at end of file diff --git a/app/views/admin/enterprise_relationships/_data.html.haml b/app/views/admin/enterprise_relationships/_data.html.haml index 7c13978e90..b6f7215afc 100644 --- a/app/views/admin/enterprise_relationships/_data.html.haml +++ b/app/views/admin/enterprise_relationships/_data.html.haml @@ -1,4 +1,2 @@ -:javascript - angular.module('ofn.admin').value('enterprise_relationships', #{render partial: "admin/json/enterprise_relationships", object: @enterprise_relationships}); - angular.module('ofn.admin').value('my_enterprises', #{render partial: "admin/json/enterprises", object: @my_enterprises}); - angular.module('ofn.admin').value('all_enterprises', #{render partial: "admin/json/enterprises", object: @all_enterprises}); += admin_inject_enterprise_relationships += admin_inject_enterprises diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 3f93027ca1..6a7dcdc7f4 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,5 +1,8 @@ -%td {{ enterprise_relationship.parent_name }} -%td permits %td {{ enterprise_relationship.child_name }} +%td + %ul + %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} + {{ EnterpriseRelationships.permission_presentation(permission.name) }} +%td {{ enterprise_relationship.parent_name }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 1a737ec3a5..86086c8781 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,9 +1,13 @@ %tr - %td - %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} - %td permits %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %td + %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} + %label + %input{type: "checkbox", "ng-model" => "permissions[permission]"} + {{ EnterpriseRelationships.permission_presentation(permission) }} + %td + %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/enterprise_roles/_data.html.haml b/app/views/admin/enterprise_roles/_data.html.haml new file mode 100644 index 0000000000..5063503803 --- /dev/null +++ b/app/views/admin/enterprise_roles/_data.html.haml @@ -0,0 +1,4 @@ += admin_inject_enterprise_roles += admin_inject_users += admin_inject_enterprises + diff --git a/app/views/admin/enterprise_roles/_enterprise_role.html.haml b/app/views/admin/enterprise_roles/_enterprise_role.html.haml new file mode 100644 index 0000000000..5e4986d599 --- /dev/null +++ b/app/views/admin/enterprise_roles/_enterprise_role.html.haml @@ -0,0 +1,5 @@ +%td {{ enterprise_role.user_email }} +%td manages +%td {{ enterprise_role.enterprise_name }} +%td.actions + %a.delete-enterprise-role.icon-trash.no-text{'ng-click' => 'delete(enterprise_role)'} diff --git a/app/views/admin/enterprise_roles/_form.html.haml b/app/views/admin/enterprise_roles/_form.html.haml new file mode 100644 index 0000000000..2f31e8fa11 --- /dev/null +++ b/app/views/admin/enterprise_roles/_form.html.haml @@ -0,0 +1,9 @@ +%tr + %td + %select{name: "enterprise_role_user_id", "ng-model" => "user_id", "ng-options" => "u.id as u.email for u in Users.users"} + %td manages + %td + %select{name: "enterprise_role_enterprise_id", "ng-model" => "enterprise_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %td.actions + %input{type: "button", value: "Create", "ng-click" => "create()"} + .errors {{ EnterpriseRoles.create_errors }} diff --git a/app/views/admin/enterprise_roles/index.html.haml b/app/views/admin/enterprise_roles/index.html.haml new file mode 100644 index 0000000000..0881a8a50e --- /dev/null +++ b/app/views/admin/enterprise_roles/index.html.haml @@ -0,0 +1,15 @@ +- content_for :page_title do + Roles + += render 'admin/shared/users_sub_menu' + +%div{"ng-app" => "ofn.admin", "ng-controller" => "AdminEnterpriseRolesCtrl"} + = render 'data' + + %input.search{"ng-model" => "query", "placeholder" => "Search"} + + %table#enterprise-roles + %tbody + = render 'form' + %tr{"ng-repeat" => "enterprise_role in EnterpriseRoles.enterprise_roles | filter:query"} + = render 'enterprise_role' diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml new file mode 100644 index 0000000000..69dd6569a8 --- /dev/null +++ b/app/views/admin/enterprises/_actions.html.haml @@ -0,0 +1,31 @@ += link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') +%br/ + += link_to_delete_enterprise enterprise +%br/ + +- if enterprise.is_primary_producer + = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) + (#{enterprise.producer_properties.count}) + %br/ + +- if enterprise.is_distributor + - if can? :admin, Spree::PaymentMethod + = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) + (#{enterprise.payment_methods.count}) + - if enterprise.payment_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} + %br/ + + - if can? :admin, Spree::ShippingMethod + = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) + (#{enterprise.shipping_methods.count}) + - if enterprise.shipping_methods.count == 0 + %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} + %br/ + +- if can? :admin, EnterpriseFee + = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) + (#{enterprise.enterprise_fees.count}) + - if enterprise.enterprise_fees.count == 0 + %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index e21e653a05..07be41dd6e 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -36,6 +36,24 @@ = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer'   = f.label :is_primary_producer, 'Producer' + .row + .alpha.eleven.columns + .three.columns.alpha + = f.label :type, 'Profile type' + .with-tip{'data-powertip' => "Full - enterprise may have products and relationships.
Single - enterprise may have products but no relationships.
Profile - enterprise has a profile but no products or relationships.
"} + %a What's this? + .two.columns + = f.radio_button :type, "full" +   + = f.label :type, "Full", value: "full" + .two.columns + = f.radio_button :type, "single" +   + = f.label :type, "Single", value: "single" + .four.columns.omega + = f.radio_button :type, "profile" +   + = f.label :type, "Profile", value: "profile" .row .three.columns.alpha %label Visible in search? diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 7a4ec13a1f..0ccd963d31 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -1,9 +1,10 @@ = admin_inject_enterprise = admin_inject_payment_methods = admin_inject_shipping_methods + .sixteen.columns.alpha{ ng: { app: 'admin.enterprises', controller: 'enterpriseCtrl' } } .eleven.columns.alpha - = render partial: 'form', :locals => { f: f } + = render 'form', f: f .one.column   .four.columns.omega - = render partial: 'sidebar', :locals => { f: f } \ No newline at end of file + = render 'sidebar', f: f diff --git a/app/views/admin/enterprises/_sidebar.html.haml b/app/views/admin/enterprises/_sidebar.html.haml index 7bd228c4c7..ac4966fa80 100644 --- a/app/views/admin/enterprises/_sidebar.html.haml +++ b/app/views/admin/enterprises/_sidebar.html.haml @@ -1,61 +1,6 @@ -.sidebar_item.four.columns.alpha#payment_methods{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ ng: { class: "paymentMethodsColor()" } } - %span.four.columns.alpha.centered Payment Methods - .four.columns.alpha.list{ ng: { class: "paymentMethodsColor()" } } - - if @payment_methods.count > 0 - -# = hidden_field_tag "enterprise[payment_method_ids][]", [] - - @payment_methods.each do |payment_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } - %a.three.columns.alpha{ href: "#{edit_admin_payment_method_path(payment_method)}" } - = payment_method.name - %span.one.column.omega - = f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_payment_method_path}", ng: { class: "paymentMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - -.sidebar_item.four.columns.alpha#shipping_methods{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ ng: { class: "shippingMethodsColor()" } } - %span.four.columns.alpha.centered Shipping Methods - .four.columns.alpha.list{ ng: { class: "shippingMethodsColor()" } } - - if @shipping_methods.count > 0 - - @shipping_methods.each do |shipping_method| - %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } - %a.three.columns.alpha{ href: "#{edit_admin_shipping_method_path(shipping_method)}" } - = shipping_method.name - %span.one.column.omega - = f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil - - else - .four.columns.alpha.list-item - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{new_admin_shipping_method_path}", ng: { class: "shippingMethodsColor()" } } - CREATE NEW - %span.icon-arrow-right - -- enterprise_fees_color = @enterprise_fees.count > 0 ? "blue" : "red" -.sidebar_item.four.columns.alpha#enterprise_fees{ ng: { show: 'Enterprise.is_distributor' } } - .four.columns.alpha.header{ class: "#{enterprise_fees_color}" } - %span.four.columns.alpha.centered Enterprise Fees - .four.columns.alpha.list{ class: "#{enterprise_fees_color}" } - - if @enterprise_fees.count > 0 - - @enterprise_fees.each do |enterprise_fee| - %a.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.admin_enterprise_fees_path}" } - %span.three.columns.alpha - = enterprise_fee.name - %span.one.column.omega -   - - else - .four.columns.alpha.list-item.red - %span.three.columns.alpha None Available - %span.one.column.omega - %span.icon-remove-sign - %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprise_fees_path}", class: "#{enterprise_fees_color}" } - CREATE NEW - %span.icon-arrow-right \ No newline at end of file +- if can? :admin, Spree::PaymentMethod + = render 'sidebar_payment_methods', f: f +- if can? :admin, Spree::ShippingMethod + = render 'sidebar_shipping_methods', f: f +- if can? :admin, EnterpriseFee + = render 'sidebar_enterprise_fees', f: f diff --git a/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml b/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml new file mode 100644 index 0000000000..50de7e07d9 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_enterprise_fees.html.haml @@ -0,0 +1,20 @@ +- enterprise_fees_color = @enterprise_fees.count > 0 ? "blue" : "red" +.sidebar_item.four.columns.alpha#enterprise_fees{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ class: "#{enterprise_fees_color}" } + %span.four.columns.alpha.centered Enterprise Fees + .four.columns.alpha.list{ class: "#{enterprise_fees_color}" } + - if @enterprise_fees.count > 0 + - @enterprise_fees.each do |enterprise_fee| + %a.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.admin_enterprise_fees_path}" } + %span.three.columns.alpha + = enterprise_fee.name + %span.one.column.omega +   + - else + .four.columns.alpha.list-item.red + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{main_app.admin_enterprise_fees_path}", class: "#{enterprise_fees_color}" } + CREATE NEW + %span.icon-arrow-right diff --git a/app/views/admin/enterprises/_sidebar_payment_methods.html.haml b/app/views/admin/enterprises/_sidebar_payment_methods.html.haml new file mode 100644 index 0000000000..8994c60be5 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_payment_methods.html.haml @@ -0,0 +1,21 @@ +.sidebar_item.four.columns.alpha#payment_methods{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ ng: { class: "paymentMethodsColor()" } } + %span.four.columns.alpha.centered Payment Methods + .four.columns.alpha.list{ ng: { class: "paymentMethodsColor()" } } + - if @payment_methods.count > 0 + -# = hidden_field_tag "enterprise[payment_method_ids][]", [] + - @payment_methods.each do |payment_method| + %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } + %a.three.columns.alpha{ href: "#{edit_admin_payment_method_path(payment_method)}" } + = payment_method.name + %span.one.column.omega + = f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil + - else + .four.columns.alpha.list-item + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{new_admin_payment_method_path}", ng: { class: "paymentMethodsColor()" } } + CREATE NEW + %span.icon-arrow-right + diff --git a/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml b/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml new file mode 100644 index 0000000000..6d4a858366 --- /dev/null +++ b/app/views/admin/enterprises/_sidebar_shipping_methods.html.haml @@ -0,0 +1,20 @@ +.sidebar_item.four.columns.alpha#shipping_methods{ ng: { show: 'Enterprise.is_distributor' } } + .four.columns.alpha.header{ ng: { class: "shippingMethodsColor()" } } + %span.four.columns.alpha.centered Shipping Methods + .four.columns.alpha.list{ ng: { class: "shippingMethodsColor()" } } + - if @shipping_methods.count > 0 + - @shipping_methods.each do |shipping_method| + %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}", ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %a.three.columns.alpha{ href: "#{edit_admin_shipping_method_path(shipping_method)}" } + = shipping_method.name + %span.one.column.omega + = f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil + - else + .four.columns.alpha.list-item + %span.three.columns.alpha None Available + %span.one.column.omega + %span.icon-remove-sign + %a.four.columns.alpha.button{ href: "#{new_admin_shipping_method_path}", ng: { class: "shippingMethodsColor()" } } + CREATE NEW + %span.icon-arrow-right + diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 38747a3252..6a8d48b24b 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -5,6 +5,6 @@ = @enterprise.name = form_for [main_app, :admin, @enterprise] do |f| - = render partial: 'ng_form', :locals => { f: f } + = render 'ng_form', f: f .twelve.columns.alpha = render partial: 'spree/admin/shared/edit_resource_links' diff --git a/app/views/admin/enterprises/index.rabl b/app/views/admin/enterprises/for_order_cycle.rabl similarity index 100% rename from app/views/admin/enterprises/index.rabl rename to app/views/admin/enterprises/for_order_cycle.rabl diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 42f12e8632..7ab6ea4d0e 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -39,29 +39,7 @@ %td= enterprise_form.check_box :visible %td= enterprise.description %td{"data-hook" => "admin_users_index_row_actions"} - = link_to_with_icon('icon-edit', 'Edit Profile', main_app.edit_admin_enterprise_path(enterprise), class: 'edit') - %br/ - = link_to_delete_enterprise enterprise - %br/ - - if enterprise.is_primary_producer - = link_to_with_icon 'icon-dashboard', 'Properties', main_app.admin_enterprise_producer_properties_path(enterprise_id: enterprise.id) - (#{enterprise.producer_properties.count}) - %br/ - - if enterprise.is_distributor - = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) - (#{enterprise.payment_methods.count}) - - if enterprise.payment_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} - %br/ - = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) - (#{enterprise.shipping_methods.count}) - - if enterprise.shipping_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} - %br/ - = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) - (#{enterprise.enterprise_fees.count}) - - if enterprise.enterprise_fees.count == 0 - %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} + = render 'actions', enterprise: enterprise - if @enterprises.empty? %tr %td{colspan: "4"}= t(:none) diff --git a/app/views/admin/json/_enterprise_relationship.rabl b/app/views/admin/json/_enterprise_relationship.rabl deleted file mode 100644 index 9be152ec5c..0000000000 --- a/app/views/admin/json/_enterprise_relationship.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @enterprise_relationship - -attributes :id, :parent_id, :child_id - -node :parent_name do |enterprise_relationship| - enterprise_relationship.parent.name -end - -node :child_name do |enterprise_relationship| - enterprise_relationship.child.name -end diff --git a/app/views/admin/json/_enterprise_relationships.rabl b/app/views/admin/json/_enterprise_relationships.rabl deleted file mode 100644 index aad55b9770..0000000000 --- a/app/views/admin/json/_enterprise_relationships.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @enterprise_relationships -extends "admin/json/enterprise_relationship" diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 3539892034..8f81b9f03f 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -23,4 +23,4 @@ = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)' %td.actions - %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text"} + %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 68dfed9f1a..e859374a3b 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -25,7 +25,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_supplier_id'} += select_tag :new_supplier_id, options_from_collection_for_select(order_cycle_producer_enterprises, :id, :name), {'ng-model' => 'new_supplier_id'} = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' @@ -50,7 +50,7 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_from_collection_for_select(Enterprise.is_distributor.managed_by(spree_current_user).by_name, :id, :name), {'ng-model' => 'new_distributor_id'} += select_tag :new_distributor_id, options_from_collection_for_select(order_cycle_hub_enterprises, :id, :name), {'ng-model' => 'new_distributor_id'} = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep index 04fc659813..fb71602eb6 100644 --- a/app/views/admin/order_cycles/show.rep +++ b/app/views/admin/order_cycles/show.rep @@ -9,7 +9,7 @@ r.element :order_cycle, @order_cycle do r.element :id end - r.list_of :exchanges, @order_cycle.exchanges.managed_by(spree_current_user).order('id ASC') do |exchange| + r.list_of :exchanges, OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_exchanges(@order_cycle).order('id ASC') do |exchange| r.element :id r.element :sender_id r.element :receiver_id diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml index 4a793689b7..31366091ef 100644 --- a/app/views/admin/shared/_enterprises_sub_menu.html.haml +++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml @@ -1,4 +1,4 @@ = content_for :sub_menu do - %ul#sub_nav.inline-menu{"data-hook" => "admin_order_sub_tabs"} + %ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"} = tab :enterprises, url: main_app.admin_enterprises_path = tab :relationships, url: main_app.admin_enterprise_relationships_path, match_path: '/enterprise_relationships' diff --git a/app/views/admin/shared/_users_sub_menu.html.haml b/app/views/admin/shared/_users_sub_menu.html.haml new file mode 100644 index 0000000000..a5911277a6 --- /dev/null +++ b/app/views/admin/shared/_users_sub_menu.html.haml @@ -0,0 +1,4 @@ += content_for :sub_menu do + %ul#sub_nav.inline-menu{"data-hook" => "admin_user_sub_tabs"} + = tab :users, url: spree.admin_users_path + = tab :roles, url: main_app.admin_enterprise_roles_path, match_path: '/enterprise_roles' diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index 5ec2f12208..c052eb1b05 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -13,19 +13,19 @@ "ng-class" => "{valid: billing.$valid, open: accordion.billing}"} %accordion-heading .row - .small-10.columns + .small-8.medium-10.columns %em %small {{ summary() | printArray }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand - %i.ofn-i_052-point-down + %i.ofn-i_052-point-down = f.fields_for :bill_address, @order.bill_address do |ba| .row @@ -40,14 +40,14 @@ .small-6.columns = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, - "ng-model" => "order.bill_address.state_id" + "ng-model" => "order.bill_address.state_id", required: true .row .small-6.columns = validated_input "Postcode", "order.bill_address.zipcode" .small-6.columns.right = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false}, "ng-model" => "order.bill_address.country_id" + "ng-model" => "order.bill_address.country_id", required: true .row .small-12.columns.text-right diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index e3b369182f..6eb149804f 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -9,23 +9,23 @@ %i.ofn-i_051-check-big Your details - %accordion-group{"is-open" => "accordion.details", + %accordion-group{"is-open" => "accordion.details", "ng-class" => "{valid: details.$valid, open: accordion.details}"} %accordion-heading .row - .small-10.columns - %em + .small-8.medium-10.columns + %em %small {{ summary() | printArray }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand - %i.ofn-i_052-point-down + %i.ofn-i_052-point-down .row .small-6.columns @@ -36,10 +36,10 @@ .row .small-6.columns = validated_input 'Email', 'order.email', type: :email, "ofn-focus" => "accordion['details']" - + .small-6.columns = validated_input 'Phone', 'order.bill_address.phone' - + .row .small-12.columns.text-right %button.primary{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 68aaf2d781..a64be0ac44 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -1,5 +1,5 @@ -= f_form_for current_order, url: main_app.update_checkout_path, - html: {name: "checkout", += f_form_for current_order, url: main_app.update_checkout_path, + html: {name: "checkout", id: "checkout_form", novalidate: true, name: "checkout"} do |f| @@ -10,16 +10,16 @@ angular.module('Darkswarm').value('order', #{render "checkout/order"}) %div - %h3.text-center.pad-top - Checkout from - = current_distributor.name + / %h3.text-center.pad-top + / Checkout from + / = current_distributor.name = render partial: "checkout/details", locals: {f: f} = render partial: "checkout/billing", locals: {f: f} = render partial: "checkout/shipping", locals: {f: f} = render partial: "checkout/payment", locals: {f: f} %p - %button.button.primary{type: :submit, + %button.button.primary{type: :submit, "ng-click" => "purchase($event)", "ng-disabled" => "checkout.$invalid"} Place order now diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 48c5e15711..b655659738 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -1,7 +1,7 @@ %fieldset#payment %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} - %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty}"} + %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty}"} %span.right %label.label.round.alert.right %i.ofn-i_009-close @@ -13,17 +13,17 @@ "ng-class" => "{valid: payment.$valid, open: accordion.payment}"} %accordion-heading .row - .small-10.columns - %em + .small-8.medium-10.columns + %em %small {{ Checkout.paymentMethod().name }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand %i.ofn-i_052-point-down @@ -38,7 +38,7 @@ "ng-model" => "order.payment_method_id" = method.name - .row{"ng-show" => "order.payment_method_id == #{method.id}"} + .row{"ng-if" => "order.payment_method_id == #{method.id}"} .small-12.columns = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index 7906824a36..27089527a2 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -13,17 +13,17 @@ "ng-class" => "{valid: shipping.$valid, open: accordion.shipping}"} %accordion-heading .row - .small-10.columns - %em + .small-8.medium-10.columns + %em %small {{ Checkout.shippingMethod().name }} - .small-2.columns.text-right + .small-4.medium-2.columns.text-right %span.accordion-up - %em + %em %small Hide - %i.ofn-i_053-point-up + %i.ofn-i_053-point-up %span.accordion-down - %em + %em %small Expand %i.ofn-i_052-point-down @@ -61,13 +61,14 @@ .small-6.columns = validated_input "City", "order.ship_address.city" .small-6.columns - = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} + = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, + "ng-model" => "order.ship_address.state_id", required: true .row .small-6.columns = validated_input "Postcode", "order.ship_address.zipcode" .small-6.columns.right = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false} + "ng-model" => "order.ship_address.country_id", required: true .row .small-6.columns = validated_input "First Name", "order.ship_address.firstname" diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 60fb8ca80a..0e03af7413 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -1,22 +1,24 @@ %orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| %fieldset - %legend Your Order + %legend Your order %table %tr - %th Produce - %td= current_order.display_item_total + %th Cart total + %td.cart-total.text-right= number_to_currency checkout_cart_total_with_adjustments(current_order) - - checkout_adjustments_for_summary(current_order, exclude_shipping: true).each do |adjustment| + - checkout_adjustments_for_summary(current_order, exclude: [:shipping, :distribution]).each do |adjustment| %tr %th= adjustment.label - %td= adjustment.display_amount.to_html + %td.text-right= adjustment.display_amount.to_html + %tr %th Shipping - %td {{ Checkout.shippingPrice() | currency }} + %td.shipping.text-right {{ Checkout.shippingPrice() | currency }} + %tr - %th Cart total - %td {{ Checkout.cartTotal() | currency }} + %th Total + %td.total.text-right {{ Checkout.cartTotal() | currency }} - if current_order.price_adjustment_totals.present? - current_order.price_adjustment_totals.each do |label, total| %tr diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 156f435970..2744627d03 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -1,21 +1,24 @@ -= inject_enterprises += inject_enterprises .darkswarm - content_for :order_cycle_form do - %strong.avenir + + %closing Checkout now + %p Order ready for - = pickup_time current_order_cycle + %strong + = pickup_time current_order_cycle = render partial: "shopping_shared/details" - + %accordion{"close-others" => "true"} %checkout.row{"ng-controller" => "CheckoutCtrl"} - .small-9.columns + .small-12.medium-8.large-9.columns - unless spree_current_user = render partial: "checkout/authentication" .row{"ng-show" => "enabled", "ng-controller" => "AccordionCtrl"} = render partial: "checkout/form" - .small-3.columns + .small-12.medium-4.large-3.columns = render partial: "checkout/summary" diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index cd220f3d53..63d2216052 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -2,16 +2,10 @@ .row .small-12.text-center.columns %h1= image_tag "ofn_logo_beta.png", title: "Open Food Network (beta)" - %h2 We're crowdfunding right now! - %h5 - %timer{"end-time" => '1407679200000'} - {{days}} days, {{hours}} hrs, {{minutes}} mins & {{seconds}} secs to go - %p Help us make Open Food Network the best it can be: - %a.button.primary{href: "http://startsomegood.com/openfoodnetwork", target:"_blank"} Support now - / %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. - %br - %ofn-modal{title: "Learn more"} - = render partial: "modals/learn_more" + %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. + + %ofn-modal{title: "Learn more"} + = render partial: "modals/learn_more" = render partial: "home/hubs" diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index afe27bc02f..f8fa116f5a 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -43,7 +43,7 @@ %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) %p %small - %a{href:'' } Site terms & conditions + %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions | %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on Github diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index 2d8fbebcb9..a61f80bf88 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -1,4 +1,4 @@ -%span.cart-span{"ng-controller" => "CartCtrl"} +%span.cart-span{"ng-controller" => "CartCtrl", "ng-class" => "{ dirty: Cart.dirty }"} %a#cart.icon{cart: true} %span.nav-branded %i.ofn-i_027-shopping-cart @@ -14,20 +14,21 @@ %li.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", "ng-controller" => "LineItemCtrl"} .row - .columns.small-6 + .columns.small-7 %small %strong {{ line_item.variant.name_to_display }} %em {{ line_item.variant.unit_to_display }} - .columns.small-3 - %small - {{line_item.quantity}} - x - {{ line_item.variant.price | currency }} - .columns.small-3.text-right + %small + {{line_item.quantity}} + %i.ofn-i_009-close + {{ line_item.variant.price | currency }} + + .columns.small-2 %small \= - %strong {{ line_item.variant.getPrice() | currency }} + %strong + .right {{ line_item.variant.getPrice() | currency }} %li.total-cart{"ng-show" => "Cart.line_items_present().length > 0"} .row @@ -36,5 +37,5 @@ .columns.small-6.text-right %strong {{ Cart.total() | currency }} - .text-right - %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Checkout now + .text-right + %a.button.primary.small{href: checkout_path, "ng-disabled" => "Cart.dirty"} Quick checkout diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 25004f97b4..16e003c83e 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -12,7 +12,7 @@ %form{action: cart_path} .small-12.medium-4.large-3.columns - %input.button.primary.right.add_to_cart{type: :submit, value: "Checkout now", + %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", "ng-disabled" => "Cart.dirty"} %div.pad-top{bindonce: true} @@ -20,12 +20,8 @@ "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | orderBy:ordering.order) track by product.id "} = render partial: "shop/products/summary" - - %span{"bo-if" => "product.hasVariants"} - = render partial: "shop/products/variants" - - .variants.row{"bo-if" => "!product.hasVariants"} - = render partial: "shop/products/master" + %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id"} %product{"ng-show" => "Products.loading"} .row.summary @@ -42,6 +38,6 @@ .row .small-12.columns %form{action: cart_path} - %input.button.primary.right.add_to_cart{type: :submit, value: "Checkout now", + %input.button.primary.right.add_to_cart{type: :submit, value: "Your shopping cart", "ng-disabled" => "Cart.dirty"} diff --git a/app/views/shop/products/_master.html.haml b/app/views/shop/products/_master.html.haml deleted file mode 100644 index f12df1b881..0000000000 --- a/app/views/shop/products/_master.html.haml +++ /dev/null @@ -1,58 +0,0 @@ -.small-12.medium-4.large-4.columns.variant-name - .table-cell - .inline {{ product.master.name_to_display }} - .bulk-buy.inline{"bo-if" => "product.group_buy"} - %i.ofn-i_056-bulk>< - %em>< - \ Bulk - --# WITHOUT GROUP BUY -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!product.group_buy"} - %input{type: :number, - min: 0, - placeholder: "0", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variants[{{product.master.id}}]", - "ng-model" => "product.master.line_item.quantity", - id: "variants_{{product.master.id}}"} - --# WITH GROUP BUY -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "product.group_buy"} - %span.bulk-input-container - %span.bulk-input - %input.bulk.first{type: :number, - min: 0, - "ng-model" => "product.master.line_item.quantity", - placeholder: "min", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variants[{{product.master.id}}]", - id: "variants_{{product.master.id}}"} - - %span.bulk-input{"bo-if" => "product.group_buy"} - %input.bulk.second{type: :number, - min: 0, - "ng-model" => "product.master.line_item.max_quantity", - placeholder: "max", - "ofn-disable-scroll" => true, - max: "{{product.on_demand && 9999 || product.count_on_hand }}", - name: "variant_attributes[{{product.master.id}}][max_quantity]"} - -.small-3.medium-1.large-1.columns.variant-unit - .table-cell - %em {{ product.master.unit_to_display }} - -.small-4.medium-2.large-2.columns.variant-price - .table-cell - %i.ofn-i_009-close - {{ product.master.price | currency }} - -#%button.graph-button{"price-breakdown" => "_", - -#"variant" => "product.master", - -#"price-breakdown-animation" => "true"} - -#%i.ofn-i-058-graph - -.small-12.medium-2.large-2.columns.total-price.text-right - .table-cell - %strong - {{ product.master.getPrice() | currency }} diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 77f43702cc..25c71f276a 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -4,8 +4,9 @@ .row.summary .small-9.medium-10.large-11.columns.summary-header - %a{"ng-click" => "triggerProductModal()"} - %h3 {{ product.name }} + %h3 + %a{"ng-click" => "triggerProductModal()"} + {{ product.name }} %em from %span diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml index 4057823d52..048dd13e73 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/show.html.haml @@ -15,7 +15,7 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ng-change" => "changeOrderCycle()", "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", - "popover-placement" => "bottom", "popover" => "When do you want to get your order?", "popover-trigger" => "openTrigger"} + "popover-placement" => "left", "popover" => "Choose when you want your order:", "popover-trigger" => "openTrigger"} diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index a73c0cea07..08a05595c5 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -92,10 +92,11 @@ %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 }} - %div.loading{ :class => "sixteen columns alpha", 'ng-show' => 'loading' } - %h4 Loading Line Items... + %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } + %img.spinner{ src: "/assets/loading.gif" } + %h1 LOADING ORDERS %div{ :class => "sixteen columns alpha", 'ng-show' => '!loading && filteredLineItems.length == 0'} - %h4{ :style => 'color:red;' } No matching line items found. + %h1#no_results No orders found. %div{ 'ng-hide' => 'loading || filteredLineItems.length == 0' } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead diff --git a/app/views/spree/admin/overview/_enterprises.html.haml b/app/views/spree/admin/overview/_enterprises.html.haml index 85dc93c2ee..757755c718 100644 --- a/app/views/spree/admin/overview/_enterprises.html.haml +++ b/app/views/spree/admin/overview/_enterprises.html.haml @@ -1,87 +1,11 @@ %div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-app' => 'ofn.admin', 'ng-controller' => "enterprisesDashboardCtrl" } - %div.header.sixteen.columns.alpha{ :class => "#{@enterprises.count > 0 ? "" : "red"}"} - %h3.thirteen.columns.alpha My Enterprises - - if @enterprises.any? - %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE NEW - - else - %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? - - if @enterprises.any? - %div.sixteen.columns.alpha.tabs - %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } HUBS - %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } PRODUCERS + = render 'enterprises_header' + - if @enterprises.empty? - %div.sixteen.columns.alpha.list-item.red - %span.text.fifteen.columns.alpha You don't have any enterprises yet. - %span.one.columns.omega - %span.icon-remove-sign - %a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } - CREATE A NEW ENTERPRISE - %span.icon-arrow-right + = render 'enterprises_none' + - else - %div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } - %div.sixteen.columns.alpha.list-title - %span.five.columns.alpha Name - %span.centered.three.columns Payment Methods - %span.centered.three.columns Shipping Methods - %span.centered.three.columns Enterprise Fees - %div.sixteen.columns.alpha.list - - @enterprises.is_distributor.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - %span.five.columns.alpha - = enterprise.name - %span.symbol.three.columns.centered - - payment_method_count = enterprise.payment_methods.count - - if payment_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Payment Methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Payment Methods." } - - else - %span.icon-ok-sign.with-tip{ title: "#{payment_method_count} Payment Method#{payment_method_count > 1 ? "s" : ""}" } - %span.symbol.three.columns.centered - - shipping_method_count = enterprise.shipping_methods.count - - if shipping_method_count < 1 && enterprise.is_distributor - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no Shipping Methods" } - - elsif enterprise.is_primary_producer - %span.icon-ok-sign.with-tip{ title: "Producers (like #{enterprise.name}) do not require Shipping Methods." } - -else - %span.icon-ok-sign.with-tip{ title: "#{shipping_method_count} Shipping Method#{shipping_method_count > 1 ? "s" : ""}" } - %span.symbol.three.columns.centered - - fee_count = enterprise.enterprise_fees.count - - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{fee_count} Fee#{fee_count > 1 ? "s" : ""}" } - - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no Enterprise Fees" } - %span.two.columns.omega.right - %span.icon-arrow-right - %div.producers_tab{ ng: { show: "activeTab == 'producers'"} } - %div.list-title.sixteen.columns.alpha - %span.five.columns.alpha Name - %span.centered.three.columns Total Products - %span.centered.three.columns Active Products - %span.centered.three.columns Products in OCs - %div.sixteen.columns.alpha.list - - @enterprises.is_primary_producer.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - %span.five.columns.alpha - = enterprise.name - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.count > 0 ? "green" : "red" }" } - = enterprise.supplied_products.not_deleted.count - %span.one.column.omega   - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.count > 0 ? "green" : "red" }" } - = enterprise.supplied_and_active_products_on_hand.count - %span.one.column.omega   - %span.symbol.three.columns.centered - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.count > 0 ? "green" : "orange" }" } - = enterprise.active_products_in_order_cycles.count - %span.one.column.omega   - %span.two.columns.omega.right - %span.icon-arrow-right - %a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } - MANAGE MY ENTERPRISES - %span.icon-arrow-right \ No newline at end of file + = render 'enterprises_tabs' + = render 'enterprises_hubs_tab' + = render 'enterprises_producers_tab' + = render 'enterprises_footer' diff --git a/app/views/spree/admin/overview/_enterprises_footer.html.haml b/app/views/spree/admin/overview/_enterprises_footer.html.haml new file mode 100644 index 0000000000..c61add38f5 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_footer.html.haml @@ -0,0 +1,3 @@ +%a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } + MANAGE MY ENTERPRISES + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_header.html.haml b/app/views/spree/admin/overview/_enterprises_header.html.haml new file mode 100644 index 0000000000..d53828ca65 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_header.html.haml @@ -0,0 +1,7 @@ +%div.header.sixteen.columns.alpha{ :class => "#{@enterprises.count > 0 ? "" : "red"}"} + %h3.thirteen.columns.alpha My Enterprises + - if @enterprises.any? + %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } + CREATE NEW + - else + %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml new file mode 100644 index 0000000000..cb177f9fb3 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -0,0 +1,43 @@ +%div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } + %div.sixteen.columns.alpha.list-title + %span.five.columns.alpha Name + - if can? :admin, Spree::PaymentMethod + %span.centered.three.columns Payment Methods + - if can? :admin, Spree::ShippingMethod + %span.centered.three.columns Shipping Methods + - if can? :admin, EnterpriseFee + %span.centered.three.columns Enterprise Fees + %div.sixteen.columns.alpha.list + - @enterprises.is_distributor.each do |enterprise| + %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + %span.five.columns.alpha + = enterprise.name + %span.symbol.three.columns.centered + - if can? :admin, Spree::PaymentMethod + - payment_method_count = enterprise.payment_methods.count + - if payment_method_count > 0 + %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } + - else + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } + - else +   + %span.symbol.three.columns.centered + - if can? :admin, Spree::ShippingMethod + - shipping_method_count = enterprise.shipping_methods.count + - if shipping_method_count > 0 + %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } + - else + %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } + - else +   + %span.symbol.three.columns.centered + - if can? :admin, EnterpriseFee + - fee_count = enterprise.enterprise_fees.count + - if fee_count > 0 + %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } + - else + %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } + - else +   + %span.two.columns.omega.right + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_none.html.haml b/app/views/spree/admin/overview/_enterprises_none.html.haml new file mode 100644 index 0000000000..c1428a6863 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_none.html.haml @@ -0,0 +1,7 @@ +%div.sixteen.columns.alpha.list-item.red + %span.text.fifteen.columns.alpha You don't have any enterprises yet. + %span.one.columns.omega + %span.icon-remove-sign +%a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } + CREATE A NEW ENTERPRISE + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml new file mode 100644 index 0000000000..d5cad103b7 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml @@ -0,0 +1,43 @@ +%div.producers_tab{ ng: { show: "activeTab == 'producers'"} } + %div.list-title.sixteen.columns.alpha + %span.five.columns.alpha Name + - if can? :admin, Spree::Product + %span.centered.three.columns Total Products + %span.centered.three.columns Active Products + - if can? :admin, OrderCycle + %span.centered.three.columns Products in OCs + %div.sixteen.columns.alpha.list + - @enterprises.is_primary_producer.each do |enterprise| + %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + + %span.five.columns.alpha + = enterprise.name + + %span.symbol.three.columns.centered + - if can? :admin, Spree::Product + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } + = enterprise.supplied_products.not_deleted.count + %span.one.column.omega   + - else +   + %span.symbol.three.columns.centered + - if can? :admin, Spree::Product + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } + = enterprise.supplied_and_active_products_on_hand.count + %span.one.column.omega   + - else +   + + %span.symbol.three.columns.centered + - if can? :admin, OrderCycle + %span.one.column.alpha   + %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } + = enterprise.active_products_in_order_cycles.count + %span.one.column.omega   + - else +   + + %span.two.columns.omega.right + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_tabs.html.haml b/app/views/spree/admin/overview/_enterprises_tabs.html.haml new file mode 100644 index 0000000000..f6e5413786 --- /dev/null +++ b/app/views/spree/admin/overview/_enterprises_tabs.html.haml @@ -0,0 +1,3 @@ +%div.sixteen.columns.alpha.tabs + %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } HUBS + %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } PRODUCERS diff --git a/app/views/spree/admin/overview/index.html.haml b/app/views/spree/admin/overview/index.html.haml index 9fe44ebcca..a46a31b404 100644 --- a/app/views/spree/admin/overview/index.html.haml +++ b/app/views/spree/admin/overview/index.html.haml @@ -5,11 +5,13 @@ = render partial: "spree/admin/overview/enterprises" - else - = render partial: "spree/admin/overview/products" + - if can? :admin, Spree::Product + = render partial: "spree/admin/overview/products" - %div.two.columns -   + %div.two.columns +   - = render partial: "spree/admin/overview/order_cycles" + - if can? :admin, OrderCycle + = render partial: "spree/admin/overview/order_cycles" = render partial: "spree/admin/overview/enterprises" diff --git a/app/views/spree/admin/payment_methods/_provider_settings.html.haml b/app/views/spree/admin/payment_methods/_provider_settings.html.haml new file mode 100644 index 0000000000..e227180fc3 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_provider_settings.html.haml @@ -0,0 +1,7 @@ +- if @payment_method.preferences.present? + %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields + %legend{ align: "center"} + = t(:provider_settings) + .preference-settings + = fields_for :payment_method, @payment_method do |payment_method_form| + = preference_fields(@payment_method, payment_method_form) \ No newline at end of file diff --git a/app/views/spree/admin/payment_methods/_providers.html.haml b/app/views/spree/admin/payment_methods/_providers.html.haml new file mode 100644 index 0000000000..77b11fb473 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_providers.html.haml @@ -0,0 +1,10 @@ +:javascript + angular.module('ofn.admin').value('paymentMethod', #{ { id: @payment_method.id, type: @payment_method.type }.to_json }) +#provider-settings{ ng: { app: "ofn.admin", controller: "ProvidersCtrl" } } + .row + .alpha.three.columns + = label :payment_method, :type, t(:provider) + .omega.eight.columns + = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, (!@object.persisted? ? { :selected => "Spree::PaymentMethod::Check"} : {}), { class: 'select2 fullwidth', 'provider-prefs-for' => "#{@object.id}"}) + + %div{"ng-include" => "include_html" } \ No newline at end of file diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index d1cdd6324f..206a336a5e 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -12,117 +12,87 @@ %div#new_product(data-hook) - +=admin_inject_producers +=admin_inject_taxons %div{ 'ng-app' => 'ofn.admin', 'ng-controller' => 'AdminProductEditCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" } + %div.sixteen.columns.alpha + %div.quick_search{ :class => "four columns alpha" } + %label{ :for => 'quick_filter' } + %br + %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } + .filter_select{ :class => "four columns" } + %label{ :for => 'producer_filter' }Producer + %br + %select{ :class => "four columns alpha", :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } + .filter_select{ :class => "four columns" } + %label{ :for => 'category_filter' }Category + %br + %select{ :class => "four columns alpha", :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} + %div{ :class => "one column" }   + .filter_clear{ :class => "three columns omega" } + %label{ :for => 'clear_all_filters' } + %br + %input.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear Filters", 'ng-click' => "resetSelectFilters()" } + %hr.sixteen.columns.alpha + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" } + %div.four.columns.alpha + %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} + %div.nine.columns + %h6{ id: "update-status-message", ng: { style: 'updateStatusMessage.style' } } + {{ updateStatusMessage.text || " " }} + %div.three.columns.omega + %div.ofn_drop_down.three.columns.omega{ '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 }} %div{ 'ng-show' => '!spree_api_key_ok' } {{ api_error_msg }} - %div.option_tab_titles{ :class => "sixteen columns alpha" } - %h6{ :class => "three columns alpha", 'ng-repeat' => "tab in optionTabs", "ng-click" => "shiftTab(tab)", "ng-class" => "tab.visible && 'selected' || !tab.visible && 'unselected'"} - {{ tab.title }} - %div.option_tabs{ :class => "sixteen columns alpha" } - %div.filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible' } - %div{ :class => "four columns alpha" } - Column: - %br.clear - %select.fullwidth{ 'ng-model' => 'filterProperty', :id => "filter_property", 'ng-options' => 'fc.name for fc in filterableColumns', 'ofn-select2-min-search' => 10 } - %div{ :class => "four columns omega" } - Filter Type: - %br.clear - %select.fullwidth{ 'ng-model' => 'filterPredicate', :id => "filter_predicate", 'ng-options' => 'ft.name for ft in filterTypes', 'ofn-select2-min-search' => 10 } - %div{ :class => "six columns omega" } - Value: - %br.clear - %input{ :class => "four columns alpha", 'ng-model' => 'filterValue', :id => "filter_value", :type => "text", 'placeholder' => 'Filter Value' } - %div{ :class => "two columns omega" } -   - %input.fullwidth{ :name => "add_filter", :value => "Apply Filter", :type => "button", "ng-click" => "addFilter({property:filterProperty,predicate:filterPredicate,value:filterValue})" } - %div.applied_filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible && currentFilters.length > 0' } - %div.applied_filter{ :class => "sixteen columns alpha", 'ng-repeat' => 'filter in currentFilters' } - %div{ :class => "four columns alpha" } - {{ filter.property.name }} - %div{ :class => "four columns omega" } - {{ filter.predicate.name }} - %div{ :class => "six columns omega" } - {{ filter.value }} - %div{ :class => "two columns omega" } - %a{ :href => "#", 'ng-click' => "removeFilter(filter)" } Remove Filter - %div.column_toggle{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.column_toggle.visible' } - %ul.column-list{ :class => "sixteen columns alpha" } - %li.column-list-item{ :class => "three columns alpha", 'ofn-toggle-column' => 'column', 'ng-repeat' => 'column in columns' } - {{ column.name }} - %hr - %div.sixteen.columns.alpha.loading{ 'ng-show' => 'loading' } - %h4 Loading Products... - %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 0' } - %h4{ :style => 'color:red;' } No matching products found. - %div.sixteen.columns.alpha{ 'ng-show' => '!loading && products.length == 500' } - %h6 Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products - %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 500 || products.length == 0' } - %div.quick_search{ :class => "five columns omega" } - %input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' } - %div.pagination{ :class => "seven columns omega" } - %div.pagenav{ :class => "two columns alpha" } - %span.first - %a{ :href => "#", 'ng-click' => "currentPage = 1", 'ng-show' => "currentPage > 1" } - « First - %span.prev - %a{ :href => "#", 'ng-click' => "currentPage = currentPage - 1", 'ng-show' => "currentPage > 1" } - ‹ Prev - %div.pagenav{ :class => "columns omega" } - %span.page{ 'ng-repeat' => "page in [] | rangeArray:minPage():maxPage()", 'ng-class' => "{current: currentPage==page}" } - %a{ :href => "#", 'ng-click' => "setPage(page)" } - {{page}} - %span{ 'ng-show' => "maxPage() < totalPages()" } ... - %div.pagenav{ :class => "two columns omega" } - %span.next - %a{ :href => "#", 'ng-click' => "currentPage = currentPage + 1", 'ng-show' => "currentPage < totalPages()" } - Next › - %span.last - %a{ :href => "#", 'ng-click' => "currentPage = totalPages()", 'ng-show' => "currentPage < totalPages()" } - Last » - %div.pagination_info{ :class => 'four columns alpha' } - Show  - %select{ 'ng-model' => 'perPage', :name => 'perPage', 'ng-options' => 'pp for pp in [25,50,100,200]'} -  per page - %br - %span Displaying {{firstVisibleProduct()}}-{{lastVisibleProduct()}} of {{totalCount()}} products - %table.index#listing_products.bulk + %div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' } + %img.spinner{ src: "/assets/loading.gif" } + %h1 LOADING PRODUCTS + %div.sixteen.columns.alpha{ 'ng-show' => '!loading && filteredProducts.length == 0' } + %h1#no_results No products found. + %div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' } + %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } %colgroup - %col - %col - %col{'style' => 'width: 20%;'} - %col{'style' => 'width: 12%;'} - %col{'style' => 'width: 12%;'} - %col{'style' => 'width: 12%;'} - %col - %col - %col - %col - %col - %col + %col.actions + %col.producer{ ng: { show: 'columns.producer.visible' } } + %col.name{ ng: { show: 'columns.name.visible' } } + %col.unit{ ng: { show: 'columns.unit.visible' } } + %col.display_as{ ng: { show: 'columns.unit.visible' } } + %col.price{ ng: { show: 'columns.price.visible'} } + %col.on_hand{ ng: { show: 'columns.on_hand.visible' } } + %col.category{ ng: { show: 'columns.category.visible' } } + %col.available_on{ ng: { show: 'columns.available_on.visible' } } + %col.actions + %col.actions + %col.actions %thead %tr %th.left-actions - %th{ 'ng-show' => 'columns.supplier.visible' } Supplier - %th{ 'ng-show' => 'columns.name.visible' } Name - %th{ 'ng-show' => 'columns.unit.visible' } Unit / Value - %th{ 'ng-show' => 'columns.unit.visible' } Display As - %th{ 'ng-show' => 'columns.price.visible' } Price - %th{ 'ng-show' => 'columns.on_hand.visible' } On Hand - %th{ 'ng-show' => 'columns.taxons.visible' } Taxons - %th{ 'ng-show' => 'columns.available_on.visible' } Av. On + %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer + %th.name{ 'ng-show' => 'columns.name.visible' } Name + %th.unit{ 'ng-show' => 'columns.unit.visible' } Unit / Value + %th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As + %th.price{ 'ng-show' => 'columns.price.visible' } Price + %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand + %th.category{ 'ng-show' => 'columns.category.visible' } Category + %th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On %th.actions %th.actions %th.actions - %tbody{ 'ng-repeat' => 'product in filteredProducts = (products | filter:query)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", 'ng-show' => "$index >= perPage*(currentPage-1) && $index < perPage*currentPage" } + %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | producer: producerFilter | category: categoryFilter | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %tr.product{ :id => "p_{{product.id}}" } %td.left-actions %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } - %td.supplier{ 'ng-show' => 'columns.supplier.visible' } - %select.select2{ 'ng-model' => 'product.supplier', :name => 'supplier', 'ofn-track-product' => 'supplier', 'ng-options' => 's.name for s in suppliers' } - %td{ 'ng-show' => 'columns.name.visible' } + %td.producer{ 'ng-show' => 'columns.producer.visible' } + %select.select2.fullwidth{ 'ng-model' => 'product.producer', :name => 'producer', 'ofn-track-product' => 'producer', 'ng-options' => 'producer.id as producer.name for producer in producers' } + %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } %select.select2{ 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } @@ -131,14 +101,14 @@ %input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' } %td.display_as{ 'ng-show' => 'columns.unit.visible' } %input{ 'ofn-display-as' => 'product.master', name: 'display_as', 'ofn-track-master' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}', ng: { hide: 'hasVariants(product)', model: 'product.master.display_as' } } - %td{ 'ng-show' => 'columns.price.visible' } + %td.price{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' } - %td{ 'ng-show' => 'columns.on_hand.visible' } + %td.on_hand{ 'ng-show' => 'columns.on_hand.visible' } %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' } - %td{ 'ng-if' => 'columns.taxons.visible' } - %input.fullwidth{ :type => 'text', 'ng-model' => 'product.taxon_ids', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'taxon_ids' } - %td{ 'ng-show' => 'columns.available_on.visible' } + %td.category{ 'ng-if' => 'columns.category.visible' } + %input.fullwidth{ :type => 'text', id: "p{{product.id}}_category", 'ng-model' => 'product.category', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category' } + %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } %td.actions %a{ 'ng-click' => 'editWarn(product)', :class => "edit-product icon-edit no-text" } @@ -146,11 +116,11 @@ %a{ 'ng-click' => 'cloneProduct(product)', :class => "clone-product icon-copy no-text" } %td.actions %a{ 'ng-click' => 'deleteProduct(product)', :class => "delete-product icon-trash no-text" } - %tr.variant{ 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } + %tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } %td.left-actions %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" } - %td{ 'ng-show' => 'columns.supplier.visible' } + %td{ 'ng-show' => 'columns.producer.visible' } %td{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" } %td.unit_value{ 'ng-show' => 'columns.unit.visible' } @@ -162,13 +132,10 @@ %td{ 'ng-show' => 'columns.on_hand.visible' } %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } %span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' } - %td{ 'ng-show' => 'columns.taxons.visible' } + %td{ 'ng-show' => 'columns.category.visible' } %td{ 'ng-show' => 'columns.available_on.visible' } %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" } %td.actions %td.actions %a{ 'ng-click' => 'deleteVariant(product,variant)', :class => "delete-variant icon-trash no-text" } - %input{ :type => 'button', :value => 'Update', 'ng-click' => 'submitProducts()'} - %span{ id: "update-status-message", 'ng-style' => 'updateStatusMessage.style' } - {{ updateStatusMessage.text }} diff --git a/app/views/spree/admin/shared/_hubs_sidebar.html.haml b/app/views/spree/admin/shared/_hubs_sidebar.html.haml index 445a45ea6c..cdd367065a 100644 --- a/app/views/spree/admin/shared/_hubs_sidebar.html.haml +++ b/app/views/spree/admin/shared/_hubs_sidebar.html.haml @@ -5,13 +5,13 @@ %span.four.columns.alpha.centered Hubs .four.columns.alpha.list{ class: "#{hubs_color}" } - if @hubs.count > 0 - = f.hidden_field :distributor_ids, :multiple => true, value: nil + = hidden_field klass, :distributor_ids, :multiple => true, value: nil - @hubs.each do |hub| %span.four.columns.alpha.list-item{ class: "#{cycle('odd','even')}" } %a.three.columns.alpha{ href: "#{main_app.edit_admin_enterprise_path(hub)}" } = hub.name %span.one.column.omega - = f.check_box :distributor_ids, { multiple: true }, hub.id, nil + = check_box klass, :distributor_ids, { multiple: true }, hub.id, nil - else .four.columns.alpha.list-item %span.three.columns.alpha None Available diff --git a/app/views/spree/order_mailer/confirm_email.text.erb b/app/views/spree/order_mailer/confirm_email.text.erb index a72ada83e2..932eaebe4c 100644 --- a/app/views/spree/order_mailer/confirm_email.text.erb +++ b/app/views/spree/order_mailer/confirm_email.text.erb @@ -10,8 +10,8 @@ Order for: <%= @order.bill_address.full_name %> <%= item.variant.sku %> <%= raw(item.variant.product.supplier.name) %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (QTY: <%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %> <% end %> ============================================================ -Subtotal: <%= @order.display_item_total %> -<% checkout_adjustments_for_summary(@order).each do |adjustment| %> +Subtotal: <%= number_to_currency checkout_cart_total_with_adjustments(@order) %> +<% checkout_adjustments_for_summary(@order, exclude: [:distribution]).each do |adjustment| %> <%= raw(adjustment.label) %> <%= adjustment.display_amount %> <% end %> Order Total: <%= @order.display_total %> diff --git a/app/views/spree/orders/_cart_item_description.html.haml b/app/views/spree/orders/_cart_item_description.html.haml index 9a516168bc..8c99781d5c 100644 --- a/app/views/spree/orders/_cart_item_description.html.haml +++ b/app/views/spree/orders/_cart_item_description.html.haml @@ -1,7 +1,7 @@ %td{'data-hook' => "cart_item_description"} - %h4= link_to variant.product.name, product_path(variant.product) + %h4= variant.product.name = variant.options_text - if @order.insufficient_stock_lines.include? line_item %span.out-of-stock = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) - %br/ \ No newline at end of file + %br/ diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index 4fdeea348b..5f8186e568 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -2,12 +2,14 @@ .darkswarm - content_for :order_cycle_form do - %strong.avenir - Order ready on - - if @order.order_cycle - = pickup_time @order.order_cycle - - else - = @order.distributor.next_collection_at + %closing Your shopping cart + %p + Order ready for + %strong + - if @order.order_cycle + = pickup_time @order.order_cycle + - else + = @order.distributor.next_collection_at = render partial: "shopping_shared/details" diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index 0460865c7e..bbee30379a 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -8,13 +8,11 @@ require 'spree/product_filters' -require 'open_food_network/searcher' Spree.config do |config| config.shipping_instructions = true config.checkout_zone = 'Australia' config.address_requires_state = true - config.searcher_class = OpenFoodNetwork::Searcher # 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded. config.default_country_id = 12 diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 33f95483cf..eadaf984ae 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -8,7 +8,6 @@ module.exports = function(config) { APPLICATION_SPEC, 'app/assets/javascripts/shared/jquery-1.8.0.js', // TODO: Can we link to Rails' jquery? 'app/assets/javascripts/shared/jquery.timeago.js', - 'app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js', 'app/assets/javascripts/shared/angular-local-storage.js', 'app/assets/javascripts/shared/bindonce.min.js', 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', diff --git a/config/routes.rb b/config/routes.rb index 75516552a4..5230e798cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,12 +33,15 @@ Openfoodnetwork::Application.routes.draw do namespace :admin do resources :order_cycles do - post :bulk_update, :on => :collection, :as => :bulk_update + post :bulk_update, on: :collection, as: :bulk_update get :clone, on: :member end resources :enterprises do - post :bulk_update, :on => :collection, :as => :bulk_update + collection do + get :for_order_cycle + post :bulk_update, as: :bulk_update + end resources :producer_properties do post :update_positions, on: :collection @@ -46,6 +49,7 @@ Openfoodnetwork::Application.routes.draw do end resources :enterprise_relationships + resources :enterprise_roles resources :enterprise_fees do post :bulk_update, :on => :collection, :as => :bulk_update @@ -107,6 +111,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] match '/admin', :to => 'admin/overview#index', :as => :admin + match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get namespace :api, :defaults => { :format => 'json' } do @@ -116,6 +121,7 @@ Spree::Core::Engine.routes.prepend do resources :products do get :managed, on: :collection + get :bulk_products, on: :collection delete :soft_delete resources :variants do diff --git a/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb b/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb new file mode 100644 index 0000000000..68d7740c64 --- /dev/null +++ b/db/migrate/20140815053659_add_unique_constraint_to_enterprise_roles.rb @@ -0,0 +1,5 @@ +class AddUniqueConstraintToEnterpriseRoles < ActiveRecord::Migration + def change + add_index :enterprise_roles, [:user_id, :enterprise_id], unique: true + end +end diff --git a/db/migrate/20140815065014_add_type_to_enterprises.rb b/db/migrate/20140815065014_add_type_to_enterprises.rb new file mode 100644 index 0000000000..0317cac350 --- /dev/null +++ b/db/migrate/20140815065014_add_type_to_enterprises.rb @@ -0,0 +1,10 @@ +class AddTypeToEnterprises < ActiveRecord::Migration + def up + add_column :enterprises, :type, :string, null: false, default: 'profile' + Enterprise.update_all type: 'full' + end + + def down + remove_column :enterprises, :type + end +end diff --git a/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb b/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb new file mode 100644 index 0000000000..1423c09db9 --- /dev/null +++ b/db/migrate/20140825023227_create_enterprise_relationship_permissions.rb @@ -0,0 +1,11 @@ +class CreateEnterpriseRelationshipPermissions < ActiveRecord::Migration + def change + create_table :enterprise_relationship_permissions do |t| + t.references :enterprise_relationship + t.string :name, null: false + end + + add_index :enterprise_relationship_permissions, :enterprise_relationship_id, name: 'index_erp_on_erid' + add_foreign_key :enterprise_relationship_permissions, :enterprise_relationships, name: 'erp_enterprise_relationship_id_fk' + end +end diff --git a/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb b/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb new file mode 100644 index 0000000000..5b4fc6e7fc --- /dev/null +++ b/db/migrate/20140826043521_prevent_duplicate_enterprise_roles.rb @@ -0,0 +1,5 @@ +class PreventDuplicateEnterpriseRoles < ActiveRecord::Migration + def change + add_index :enterprise_roles, [:enterprise_id, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7b31f581f1..dbb50d6b33 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140723023713) do +ActiveRecord::Schema.define(:version => 20140826043521) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -207,6 +207,13 @@ ActiveRecord::Schema.define(:version => 20140723023713) do add_index "enterprise_groups_enterprises", ["enterprise_group_id"], :name => "index_enterprise_groups_enterprises_on_enterprise_group_id" add_index "enterprise_groups_enterprises", ["enterprise_id"], :name => "index_enterprise_groups_enterprises_on_enterprise_id" + create_table "enterprise_relationship_permissions", :force => true do |t| + t.integer "enterprise_relationship_id" + t.string "name", :null => false + end + + add_index "enterprise_relationship_permissions", ["enterprise_relationship_id"], :name => "index_erp_on_erid" + create_table "enterprise_relationships", :force => true do |t| t.integer "parent_id" t.integer "child_id" @@ -221,7 +228,9 @@ ActiveRecord::Schema.define(:version => 20140723023713) do t.integer "enterprise_id" end + add_index "enterprise_roles", ["enterprise_id", "user_id"], :name => "index_enterprise_roles_on_enterprise_id_and_user_id", :unique => true add_index "enterprise_roles", ["enterprise_id"], :name => "index_enterprise_roles_on_enterprise_id" + add_index "enterprise_roles", ["user_id", "enterprise_id"], :name => "index_enterprise_roles_on_user_id_and_enterprise_id", :unique => true add_index "enterprise_roles", ["user_id"], :name => "index_enterprise_roles_on_user_id" create_table "enterprises", :force => true do |t| @@ -240,8 +249,8 @@ ActiveRecord::Schema.define(:version => 20140723023713) do t.integer "address_id" t.string "pickup_times" t.string "next_collection_at" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "distributor_info" t.string "logo_file_name" t.string "logo_content_type" @@ -255,6 +264,7 @@ ActiveRecord::Schema.define(:version => 20140723023713) do t.string "facebook" t.string "instagram" t.string "linkedin" + t.string "type", :default => "profile", :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -1050,6 +1060,8 @@ ActiveRecord::Schema.define(:version => 20140723023713) do add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" + add_foreign_key "enterprise_relationship_permissions", "enterprise_relationships", name: "erp_enterprise_relationship_id_fk" + add_foreign_key "enterprise_relationships", "enterprises", name: "enterprise_relationships_child_id_fk", column: "child_id" add_foreign_key "enterprise_relationships", "enterprises", name: "enterprise_relationships_parent_id_fk", column: "parent_id" diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb new file mode 100644 index 0000000000..c8070f2ab2 --- /dev/null +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -0,0 +1,88 @@ +require 'open_food_network/enterprise_fee_applicator' + +module OpenFoodNetwork + class EnterpriseFeeCalculator + def initialize(distributor=nil, order_cycle=nil) + @distributor = distributor + @order_cycle = order_cycle + end + + + def fees_for(variant) + per_item_enterprise_fee_applicators_for(variant).sum do |applicator| + calculate_fee_for variant, applicator + end + end + + def fees_by_type_for(variant) + per_item_enterprise_fee_applicators_for(variant).inject({}) do |fees, applicator| + fees[applicator.enterprise_fee.fee_type.to_sym] ||= 0 + fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator + fees + end.select { |fee_type, amount| amount > 0 } + end + + + def create_line_item_adjustments_for(line_item) + variant = line_item.variant + @distributor = line_item.order.distributor + @order_cycle = line_item.order.order_cycle + + per_item_enterprise_fee_applicators_for(variant).each do |applicator| + applicator.create_line_item_adjustment(line_item) + end + end + + def create_order_adjustments_for(order) + @distributor = order.distributor + @order_cycle = order.order_cycle + + per_order_enterprise_fee_applicators_for(order).each do |applicator| + applicator.create_order_adjustment(order) + end + end + + + private + + def calculate_fee_for(variant, applicator) + # Spree's Calculator interface accepts Orders or LineItems, + # so we meet that interface with a struct. + # Amount is faked, this is a method on LineItem + line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price + applicator.enterprise_fee.compute_amount(line_item) + end + + def per_item_enterprise_fee_applicators_for(variant) + fees = [] + + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| + exchange.enterprise_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') + end + + fees + end + + def per_order_enterprise_fee_applicators_for(order) + fees = [] + + @order_cycle.exchanges_supplying(order).each do |exchange| + exchange.enterprise_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') + end + + fees + end + end +end diff --git a/lib/open_food_network/option_value_namer.rb b/lib/open_food_network/option_value_namer.rb index 101fae427c..2c2383906d 100644 --- a/lib/open_food_network/option_value_namer.rb +++ b/lib/open_food_network/option_value_namer.rb @@ -1,27 +1,34 @@ module OpenFoodNetwork - class OptionValueNamer < Struct.new(:variant) - def name - value, unit = self.option_value_value_unit - separator = self.value_scaled? ? '' : ' ' + class OptionValueNamer + def initialize(variant = nil) + @variant = variant + end + + def name(obj = nil) + @variant = obj unless obj.nil? + value, unit = option_value_value_unit + separator = value_scaled? ? '' : ' ' name_fields = [] name_fields << "#{value}#{separator}#{unit}" if value.present? && unit.present? - name_fields << variant.unit_description if variant.unit_description.present? + name_fields << @variant.unit_description if @variant.unit_description.present? name_fields.join ' ' end + private + def value_scaled? - variant.product.variant_unit_scale.present? + @variant.product.variant_unit_scale.present? end def option_value_value_unit - if variant.unit_value.present? - if %w(weight volume).include? variant.product.variant_unit - value, unit_name = self.option_value_value_unit_scaled + if @variant.unit_value.present? + if %w(weight volume).include? @variant.product.variant_unit + value, unit_name = option_value_value_unit_scaled else - value = variant.unit_value - unit_name = variant.product.variant_unit_name + value = @variant.unit_value + unit_name = @variant.product.variant_unit_name unit_name = unit_name.pluralize if value > 1 end @@ -35,9 +42,9 @@ module OpenFoodNetwork end def option_value_value_unit_scaled - unit_scale, unit_name = self.scale_for_unit_value + unit_scale, unit_name = scale_for_unit_value - value = variant.unit_value / unit_scale + value = @variant.unit_value / unit_scale [value, unit_name] end @@ -48,10 +55,10 @@ module OpenFoodNetwork # Find the largest available unit where unit_value comes to >= 1 when expressed in it. # If there is none available where this is true, use the smallest available unit. - unit = units[variant.product.variant_unit].select { |scale, unit_name| - variant.unit_value / scale >= 1 + unit = units[@variant.product.variant_unit].select { |scale, unit_name| + @variant.unit_value / scale >= 1 }.to_a.last - unit = units[variant.product.variant_unit].first if unit.nil? + unit = units[@variant.product.variant_unit].first if unit.nil? unit end diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb new file mode 100644 index 0000000000..3d5df1e804 --- /dev/null +++ b/lib/open_food_network/permissions.rb @@ -0,0 +1,60 @@ +module OpenFoodNetwork + class Permissions + def initialize(user) + @user = user + end + + # Find enterprises that an admin is allowed to add to an order cycle + def order_cycle_enterprises + managed_and_related_enterprises_with :add_to_order_cycle + end + + # Find the exchanges of an order cycle that an admin can manage + def order_cycle_exchanges(order_cycle) + enterprises = managed_enterprises + related_enterprises_with(:add_to_order_cycle) + order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) + end + + def managed_products + managed_enterprise_products_ids = managed_enterprise_products.pluck :id + permitted_enterprise_products_ids = related_enterprise_products.pluck :id + Spree::Product.where('id IN (?)', managed_enterprise_products_ids + permitted_enterprise_products_ids) + end + + def managed_product_enterprises + managed_and_related_enterprises_with :manage_products + end + + + private + + def managed_enterprises + Enterprise.managed_by(@user) + end + + def related_enterprises_with(permission) + parent_ids = EnterpriseRelationship. + permitting(managed_enterprises). + with_permission(permission). + pluck(:parent_id) + + Enterprise.where('id IN (?)', parent_ids) + end + + def managed_and_related_enterprises_with(permission) + managed_enterprise_ids = managed_enterprises.pluck :id + permitted_enterprise_ids = related_enterprises_with(permission).pluck :id + + Enterprise.where('id IN (?)', managed_enterprise_ids + permitted_enterprise_ids) + end + + + def managed_enterprise_products + Spree::Product.managed_by(@user) + end + + def related_enterprise_products + Spree::Product.where('supplier_id IN (?)', related_enterprises_with(:manage_products)) + end + end +end diff --git a/lib/open_food_network/searcher.rb b/lib/open_food_network/searcher.rb deleted file mode 100644 index 83781ea475..0000000000 --- a/lib/open_food_network/searcher.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spree/core/search/base' - -module OpenFoodNetwork - class Searcher < Spree::Core::Search::Base - - # Do not perform pagination - def retrieve_products - @products_scope = get_base_scope - curr_page = page || 1 - - @products = @products_scope.includes([:master]) - end - - def get_base_scope - base_scope = super - - # The concern of separating products by distributor and order cycle is dealt with in - # a few other places: OpenFoodNetwork::SplitProductsByDistribution (for splitting the main - # product display) and Spree::BaseHelper decorator (for taxon counts). - - base_scope = base_scope.in_supplier_or_distributor(enterprise_id) if enterprise_id - base_scope = base_scope.in_supplier(supplier_id) if supplier_id - base_scope = base_scope.in_distributor(distributor_id) if distributor_id - - base_scope - end - - - def prepare(params) - super(params) - @properties[:enterprise_id] = params[:enterprise_id] - @properties[:supplier_id] = params[:supplier_id] - @properties[:distributor_id] = params[:distributor_id] - end - - end -end diff --git a/public/404.html b/public/404.html index 77830da9f7..b650949066 100644 --- a/public/404.html +++ b/public/404.html @@ -28,7 +28,7 @@
- +

It seems the page you're looking for is in a grump.

Return home

diff --git a/public/422.html b/public/422.html index b222475557..c6a93d7ab6 100644 --- a/public/422.html +++ b/public/422.html @@ -28,7 +28,7 @@
- +

The change you wanted was rejected. Maybe you tried to change something you don't have access to.

Return home

diff --git a/public/500.html b/public/500.html index 30379bc44d..5c7fa05f07 100644 --- a/public/500.html +++ b/public/500.html @@ -28,7 +28,7 @@
- +

We're sorry, but something went wrong.
Try refreshing the page, or

Return home

diff --git a/public/Terms-of-service.pdf b/public/Terms-of-service.pdf new file mode 100644 index 0000000000..603ffc8f86 Binary files /dev/null and b/public/Terms-of-service.pdf differ diff --git a/script/restore.sh b/script/restore.sh index 00457225ac..ea87a84631 100755 --- a/script/restore.sh +++ b/script/restore.sh @@ -6,4 +6,4 @@ set -e echo "drop database open_food_network_dev" | psql -h localhost -U ofn open_food_network_test echo "create database open_food_network_dev" | psql -h localhost -U ofn open_food_network_test -zcat $1 |psql -h localhost -U ofn open_food_network_dev +gunzip -c $1 |psql -h localhost -U ofn open_food_network_dev diff --git a/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/spec/controllers/spree/admin/payment_methods_controller_spec.rb new file mode 100644 index 0000000000..f3266f69c2 --- /dev/null +++ b/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Spree::Admin::PaymentMethodsController do + context "Requesting provider preference fields" do + let(:enterprise) { create(:distributor_enterprise) } + let(:user) do + new_user = create(:user, email: 'enterprise@hub.com', password: 'blahblah', :password_confirmation => 'blahblah', ) + new_user.spree_roles = [] # for some reason unbeknown to me, this new user gets admin permissions by default. + new_user.enterprise_roles.build(enterprise: enterprise).save + new_user.save + new_user + end + + before do + controller.stub spree_current_user: user + end + + context "on an existing payment method" do + let(:payment_method) { create(:payment_method) } + + context "where I have permission" do + before do + payment_method.distributors << user.enterprises.is_distributor.first + end + + context "without an altered provider type" do + it "renders provider settings with same payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::PaymentMethod::Check" + } + expect(assigns(:payment_method)).to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end + end + + context "with an altered provider type" do + it "renders provider settings with a different payment method" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::Gateway::Bogus" + } + expect(assigns(:payment_method)).not_to eq payment_method + expect(response).to render_template partial: '_provider_settings' + end + end + end + + context "where I do not have permission" do + before do + payment_method.distributors = [] + end + + it "renders unauthorised" do + spree_get :show_provider_preferences, { + pm_id: payment_method.id, + provider_type: "Spree::PaymentMethod::Check" + } + expect(assigns(:payment_method)).to eq payment_method + expect(flash[:error]).to eq "Authorization Failure" + end + end + end + + context "on a new payment method" do + it "renders provider settings with a new payment method of type" do + spree_get :show_provider_preferences, { + pm_id: "", + provider_type: "Spree::Gateway::Bogus" + } + expect(assigns(:payment_method)).to be_a_new Spree::Gateway::Bogus + expect(response).to render_template partial: '_provider_settings' + end + end + end +end \ No newline at end of file diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index f05be2bb69..9746ffa0e2 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -1,11 +1,28 @@ require 'spec_helper' describe Spree::Admin::ProductsController do - context "Creating a new product" do + describe "updating a product we do not have access to" do + let(:s_managed) { create(:enterprise) } + let(:s_unmanaged) { create(:enterprise) } + let(:p) { create(:simple_product, supplier: s_unmanaged, name: 'Peas') } + before do - login_as_admin + login_as_enterprise_user [s_managed] + spree_post :bulk_update, {"products" => [{"id" => p.id, "name" => "Pine nuts"}]} end + it "denies access" do + response.should redirect_to "http://test.host/unauthorized" + end + + it "does not update any product" do + p.reload.name.should_not == "Pine nuts" + end + end + + context "creating a new product" do + before { login_as_admin } + it "redirects to bulk_edit when the user hits 'create'" do s = create(:supplier_enterprise) t = create(:taxon) diff --git a/spec/factories.rb b/spec/factories.rb index 64afa6a21a..849d38abf3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -83,6 +83,7 @@ FactoryGirl.define do factory :enterprise, :class => Enterprise do sequence(:name) { |n| "Enterprise #{n}" } + type 'full' description 'enterprise' long_description '

Hello, world!

This is a paragraph.

' email 'enterprise@example.com' @@ -102,6 +103,9 @@ FactoryGirl.define do factory :enterprise_relationship do end + factory :enterprise_role do + end + factory :enterprise_group, :class => EnterpriseGroup do name 'Enterprise group' description 'this is a group' diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 963cd04f8d..a8fe6cbda7 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -24,7 +24,7 @@ feature %q{ it "displays a message when number of line items is zero" do visit '/admin/orders/bulk_management' - page.should have_text "No matching line items found." + page.should have_text "No orders found." end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 79cff455a7..9f530f860c 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -18,31 +18,14 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "product_name", with: p1.name, :visible => true - page.should have_field "product_name", with: p2.name, :visible => true + expect(page).to have_field "product_name", with: p1.name, :visible => true + expect(page).to have_field "product_name", with: p2.name, :visible => true end it "displays a message when number of products is zero" do visit '/admin/products/bulk_edit' - page.should have_text "No matching products found." - end - - pending "displays a message when number of products is too great" do - 501.times { FactoryGirl.create(:simple_product) } - - visit '/admin/products/bulk_edit' - - page.should have_text "Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products" - end - - it "displays pagination information" do - p1 = FactoryGirl.create(:product) - p2 = FactoryGirl.create(:product) - - visit '/admin/products/bulk_edit' - - page.should have_text "Displaying 1-2 of 2 products" + expect(page).to have_text "No products found." end it "displays a select box for suppliers, with the appropriate supplier selected" do @@ -54,8 +37,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "supplier", with_options: [s1.name,s2.name,s3.name], selected: s2.name - page.should have_select "supplier", with_options: [s1.name,s2.name,s3.name], selected: s3.name + expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s2.name + expect(page).to have_select "producer", with_options: [s1.name,s2.name,s3.name], selected: s3.name end it "displays a date input for available_on for each product, formatted to yyyy-mm-dd hh:mm:ss" do @@ -63,11 +46,11 @@ feature %q{ p2 = FactoryGirl.create(:product, available_on: Date.today-1) visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_field "available_on", with: p1.available_on.strftime("%F %T") - page.should have_field "available_on", with: p2.available_on.strftime("%F %T") + expect(page).to have_field "available_on", with: p1.available_on.strftime("%F %T") + expect(page).to have_field "available_on", with: p2.available_on.strftime("%F %T") end it "displays a price input for each product without variants (ie. for master variant)" do @@ -82,9 +65,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "price", with: "22.0" - page.should have_field "price", with: "44.0" - page.should_not have_field "price", with: "66.0", visible: true + expect(page).to have_field "price", with: "22.0" + expect(page).to have_field "price", with: "44.0" + expect(page).to have_no_field "price", with: "66.0", visible: true end it "displays an on hand count input for each product (ie. for master variant) if no regular variants exist" do @@ -97,9 +80,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should_not have_selector "span[name='on_hand']", text: "0" - page.should have_field "on_hand", with: "15" - page.should have_field "on_hand", with: "12" + expect(page).to have_no_selector "span[name='on_hand']", text: "0" + expect(page).to have_field "on_hand", with: "15" + expect(page).to have_field "on_hand", with: "12" end it "displays an on hand count in a span for each product (ie. for master variant) if other variants exist" do @@ -113,9 +96,9 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should_not have_field "on_hand", with: "15" - page.should have_selector "span[name='on_hand']", text: "4" - page.should have_field "on_hand", with: "12" + expect(page).to have_no_field "on_hand", with: "15" + expect(page).to have_selector "span[name='on_hand']", text: "4" + expect(page).to have_field "on_hand", with: "12" end it "displays 'on demand' for the on hand count when the product is available on demand" do @@ -124,8 +107,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "span[name='on_hand']", text: "On demand" - page.should_not have_field "on_hand", visible: true + expect(page).to have_selector "span[name='on_hand']", text: "On demand" + expect(page).to have_no_field "on_hand", visible: true end it "displays 'on demand' for any variant that is available on demand" do @@ -136,10 +119,10 @@ feature %q{ visit '/admin/products/bulk_edit' first("a.view-variants").trigger('click') - page.should_not have_selector "span[name='on_hand']", text: "On demand", visible: true - page.should have_field "variant_on_hand", with: "4" - page.should_not have_field "variant_on_hand", with: "", visible: true - page.should have_selector "span[name='variant_on_hand']", text: "On demand" + expect(page).to have_no_selector "span[name='on_hand']", text: "On demand", visible: true + expect(page).to have_field "variant_on_hand", with: "4" + expect(page).to have_no_field "variant_on_hand", with: "", visible: true + expect(page).to have_selector "span[name='variant_on_hand']", text: "On demand" end it "displays a select box for the unit of measure for the product's variants" do @@ -147,7 +130,7 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Weight (g)" + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (g)" end it "displays a text field for the item name when unit is set to 'Items'" do @@ -155,8 +138,8 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Items" - page.should have_field "variant_unit_name", with: "packet" + expect(page).to have_select "variant_unit_with_scale", selected: "Items" + expect(page).to have_field "variant_unit_name", with: "packet" end end @@ -170,13 +153,13 @@ feature %q{ v2 = FactoryGirl.create(:variant, display_name: "something2" ) visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "product_name", with: v1.product.name - page.should have_field "product_name", with: v2.product.name - page.should have_field "variant_display_name", with: v1.display_name - page.should have_field "variant_display_name", with: v2.display_name + expect(page).to have_field "product_name", with: v1.product.name + expect(page).to have_field "product_name", with: v2.product.name + expect(page).to have_field "variant_display_name", with: v1.display_name + expect(page).to have_field "variant_display_name", with: v2.display_name end it "displays an on_hand input (for each variant) for each product" do @@ -187,9 +170,9 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "span[name='on_hand']", text: "21" - page.should have_field "variant_on_hand", with: "15" - page.should have_field "variant_on_hand", with: "6" + expect(page).to have_selector "span[name='on_hand']", text: "21" + expect(page).to have_field "variant_on_hand", with: "15" + expect(page).to have_field "variant_on_hand", with: "6" end @@ -201,9 +184,9 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "price", with: "2.0", visible: false - page.should have_field "variant_price", with: "12.75" - page.should have_field "variant_price", with: "2.5" + expect(page).to have_field "price", with: "2.0", visible: false + expect(page).to have_field "variant_price", with: "12.75" + expect(page).to have_field "variant_price", with: "2.5" end it "displays a unit value field (for each variant) for each product" do @@ -214,10 +197,10 @@ feature %q{ visit '/admin/products/bulk_edit' all("a.view-variants").each { |e| e.trigger('click') } - page.should have_field "variant_unit_value_with_description", with: "1.2 (small bag)" - page.should have_field "variant_unit_value_with_description", with: "4.8 (large bag)" - page.should have_field "variant_display_as", with: "bag" - page.should have_field "variant_display_as", with: "bin" + expect(page).to have_field "variant_unit_value_with_description", with: "1.2 (small bag)" + expect(page).to have_field "variant_unit_value_with_description", with: "4.8 (large bag)" + expect(page).to have_field "variant_display_as", with: "bag" + expect(page).to have_field "variant_display_as", with: "bin" end end @@ -232,7 +215,7 @@ feature %q{ visit '/admin/products/bulk_edit' find("a", text: "NEW PRODUCT").click - page.should have_content 'NEW PRODUCT' + expect(page).to have_content 'NEW PRODUCT' fill_in 'product_name', :with => 'Big Bag Of Apples' select(s.name, :from => 'product_supplier_id') @@ -240,9 +223,9 @@ feature %q{ select taxon.name, from: 'product_primary_taxon_id' click_button 'Create' - URI.parse(current_url).path.should == '/admin/products/bulk_edit' - flash_message.should == 'Product "Big Bag Of Apples" has been successfully created!' - page.should have_field "product_name", with: 'Big Bag Of Apples' + expect(URI.parse(current_url).path).to eq '/admin/products/bulk_edit' + expect(flash_message).to eq 'Product "Big Bag Of Apples" has been successfully created!' + expect(page).to have_field "product_name", with: 'Big Bag Of Apples' end @@ -253,13 +236,13 @@ feature %q{ visit '/admin/products/bulk_edit' # I should not see an add variant button - page.should_not have_selector 'a.add-variant', visible: true + expect(page).to have_no_selector 'a.add-variant', visible: true # When I set the unit select "Weight (kg)", from: "variant_unit_with_scale" # I should see an add variant button - page.should have_selector 'a.add-variant', visible: true + expect(page).to have_selector 'a.add-variant', visible: true # When I add three variants page.find('a.add-variant', visible: true).trigger('click') @@ -267,44 +250,44 @@ feature %q{ page.find('a.add-variant', visible: true).trigger('click') # They should be added, and should see no edit buttons - page.all("tr.variant").count.should == 3 - page.should_not have_selector "a.edit-variant", visible: true + variant_count = page.all("tr.variant").count + expect(variant_count).to eq 3 + expect(page).to have_no_selector "a.edit-variant", visible: true # When I remove two, they should be removed page.all('a.delete-variant').first.click page.all('a.delete-variant').first.click - page.all("tr.variant").count.should == 1 + variant_count = page.all("tr.variant").count + expect(variant_count).to eq 1 # When I fill out variant details and hit update fill_in "variant_display_name", with: "Case of 12 Bottles" - fill_in "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + fill_in "variant_unit_value_with_description", with: "3 (12x250 mL bottles)" fill_in "variant_display_as", with: "Case" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" - click_button 'Update' + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - page.find("span#update-status-message").should have_content "Update complete" + updated_variant = Spree::Variant.where(deleted_at: nil).last + expect(updated_variant.display_name).to eq "Case of 12 Bottles" + expect(updated_variant.unit_value).to eq 3000 + expect(updated_variant.unit_description).to eq "(12x250 mL bottles)" + expect(updated_variant.display_as).to eq "Case" + expect(updated_variant.price).to eq 4.0 + expect(updated_variant.on_hand).to eq 10 # Then I should see edit buttons for the new variant - page.should have_selector "a.edit-variant", visible: true - - # And the variants should be saved - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_display_name", with: "Case of 12 Bottles" - page.should have_field "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" - page.should have_field "variant_display_as", with: "Case" - page.should have_field "variant_price", with: "4.0" - page.should have_field "variant_on_hand", with: "10" + expect(page).to have_selector "a.edit-variant", visible: true end scenario "updating a product with no variants (except master)" do s1 = FactoryGirl.create(:supplier_enterprise) s2 = FactoryGirl.create(:supplier_enterprise) - p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1) + t1 = FactoryGirl.create(:taxon) + t2 = FactoryGirl.create(:taxon) + p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2) p.price = 10.0 p.on_hand = 6; p.save! @@ -313,36 +296,43 @@ feature %q{ visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click + first("div#columns_dropdown div.menu div.menu_item", text: "Category").click - page.should have_field "product_name", with: p.name - page.should have_select "supplier", selected: s1.name - page.should have_field "available_on", with: p.available_on.strftime("%F %T") - page.should have_field "price", with: "10.0" - page.should have_select "variant_unit_with_scale", selected: "Volume (L)" - page.should have_field "on_hand", with: "6" + within "tr#p_#{p.id}" do + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: s1.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + save_screenshot '/Users/rob/Desktop/ss.png' + expect(page).to have_selector "div#s2id_p#{p.id}_category a.select2-choice" + expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" + expect(page).to have_field "on_hand", with: "6" - fill_in "product_name", with: "Big Bag Of Potatoes" - select(s2.name, :from => 'supplier') - fill_in "available_on", with: (Date.today-3).strftime("%F %T") - fill_in "price", with: "20" - select "Weight (kg)", from: "variant_unit_with_scale" - fill_in "on_hand", with: "18" - fill_in "display_as", with: "Big Bag" + fill_in "product_name", with: "Big Bag Of Potatoes" + select s2.name, :from => 'producer' + fill_in "available_on", with: (Date.today-3).strftime("%F %T") + fill_in "price", with: "20" + select "Weight (kg)", from: "variant_unit_with_scale" + select2_select t1.name, from: "p#{p.id}_category" + fill_in "on_hand", with: "18" + fill_in "display_as", with: "Big Bag" + end - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - - page.should have_field "product_name", with: "Big Bag Of Potatoes" - page.should have_select "supplier", selected: s2.name - page.should have_field "available_on", with: (Date.today-3).strftime("%F %T"), visible: false - page.should have_field "price", with: "20.0" - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "on_hand", with: "18" - page.should have_field "display_as", with: "Big Bag" + p.reload + expect(p.name).to eq "Big Bag Of Potatoes" + expect(p.supplier).to eq s2 + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg + expect(p.available_on).to eq 3.days.ago.beginning_of_day + expect(p.master.display_as).to eq "Big Bag" + expect(p.price).to eq 20.0 + expect(p.on_hand).to eq 18 + expect(p.primary_taxon).to eq t1 end scenario "updating a product with a variant unit of 'items'" do @@ -352,18 +342,18 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (kg)" select "Items", from: "variant_unit_with_scale" fill_in "variant_unit_name", with: "loaf" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - - page.should have_select "variant_unit_with_scale", selected: "Items" - page.should have_field "variant_unit_name", with: "loaf" + p.reload + expect(p.variant_unit).to eq "items" + expect(p.variant_unit_scale).to be_nil + expect(p.variant_unit_name).to eq "loaf" end scenario "setting a variant unit on a product that has none" do @@ -374,20 +364,21 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: '' + expect(page).to have_select "variant_unit_with_scale", selected: '' select "Weight (kg)", from: "variant_unit_with_scale" first("a.view-variants").trigger('click') fill_in "variant_unit_value_with_description", with: '123 abc' - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - first("a.view-variants").trigger('click') - - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "variant_unit_value_with_description", with: "123 abc" + p.reload + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg + v.reload + expect(v.unit_value).to eq 123000 # 123 kg in g + expect(v.unit_description).to eq "abc" end describe "setting the master unit value for a product without variants" do @@ -398,25 +389,20 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_select "variant_unit_with_scale", selected: '' - page.should_not have_field "master_unit_value_with_description", visible: true + expect(page).to have_select "variant_unit_with_scale", selected: '' + expect(page).to have_no_field "master_unit_value_with_description", visible: true select "Weight (kg)", from: "variant_unit_with_scale" fill_in "master_unit_value_with_description", with: '123 abc' - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" - - visit '/admin/products/bulk_edit' - - page.should have_select "variant_unit_with_scale", selected: "Weight (kg)" - page.should have_field "master_unit_value_with_description", with: "123 abc" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." p.reload - p.variant_unit.should == 'weight' - p.variant_unit_scale.should == 1000 - p.master.unit_value.should == 123000 - p.master.unit_description.should == 'abc' + expect(p.variant_unit).to eq 'weight' + expect(p.variant_unit_scale).to eq 1000 + expect(p.master.unit_value).to eq 123000 + expect(p.master.unit_description).to eq 'abc' end it "does not show the field when the product has variants" do @@ -428,7 +414,7 @@ feature %q{ visit '/admin/products/bulk_edit' select "Weight (kg)", from: "variant_unit_with_scale" - page.should_not have_field "master_unit_value_with_description", visible: true + expect(page).to have_no_field "master_unit_value_with_description", visible: true end end @@ -442,30 +428,29 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" first("a.view-variants").trigger('click') - page.should have_field "variant_price", with: "3.0" - page.should have_field "variant_unit_value_with_description", with: "250 (bottle)" - page.should have_field "variant_on_hand", with: "9" - page.should have_selector "span[name='on_hand']", text: "9" + expect(page).to have_field "variant_price", with: "3.0" + expect(page).to have_field "variant_unit_value_with_description", with: "250 (bottle)" + expect(page).to have_field "variant_on_hand", with: "9" + expect(page).to have_selector "span[name='on_hand']", text: "9" + select "Volume (L)", from: "variant_unit_with_scale" fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" - fill_in "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + fill_in "variant_unit_value_with_description", with: "2 (8x250 mL bottles)" - page.should have_selector "span[name='on_hand']", text: "10" + expect(page).to have_selector "span[name='on_hand']", text: "10" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_price", with: "4.0" - page.should have_field "variant_on_hand", with: "10" - page.should have_field "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)" + v.reload + expect(v.price).to eq 4.0 + expect(v.on_hand).to eq 10 + expect(v.unit_value).to eq 2 # 2L in L + expect(v.unit_description).to eq "(8x250 mL bottles)" end scenario "updating delegated attributes of variants in isolation" do @@ -475,21 +460,18 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" first("a.view-variants").trigger('click') - page.should have_field "variant_price", with: "3.0" + expect(page).to have_field "variant_price", with: "3.0" fill_in "variant_price", with: "10.0" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" - first("a.view-variants").trigger('click') - - page.should have_field "variant_price", with: "10.0" + v.reload + expect(v.price).to eq 10.0 end scenario "updating a product mutiple times without refresh" do @@ -498,26 +480,32 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field "product_name", with: "original name" + expect(page).to have_field "product_name", with: "original name" fill_in "product_name", with: "new name 1" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." + p.reload + expect(p.name).to eq "new name 1" fill_in "product_name", with: "new name 2" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." + p.reload + expect(p.name).to eq "new name 2" fill_in "product_name", with: "original name" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." + p.reload + expect(p.name).to eq "original name" end scenario "updating a product after cloning a product" do - FactoryGirl.create(:product, :name => "product 1") + p = FactoryGirl.create(:product, :name => "product 1") login_to_admin_section visit '/admin/products/bulk_edit' @@ -526,54 +514,42 @@ feature %q{ fill_in "product_name", :with => "new product name" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." + p.reload + expect(p.name).to eq "new product name" end scenario "updating when no changes have been made" do Capybara.using_wait_time(2) do FactoryGirl.create(:product, :name => "product 1") - FactoryGirl.create(:product, :name => "product 2") login_to_admin_section visit '/admin/products/bulk_edit' - click_button 'Update' - page.find("span#update-status-message").should have_content "No changes to update." + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "No changes to save." end end scenario "updating when a filter has been applied" do - p1 = FactoryGirl.create(:simple_product, :name => "product1") - p2 = FactoryGirl.create(:simple_product, :name => "product2") + s1 = create(:supplier_enterprise) + s2 = create(:supplier_enterprise) + p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "div.option_tab_titles h6", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click + select2_select s1.name, from: "producer_filter" - select2_select "Name", from: "filter_property" - select2_select "Contains", from: "filter_predicate" - fill_in "filter_value", :with => "1" - click_button "Apply Filter" - page.should_not have_field "product_name", with: p2.name + expect(page).to have_no_field "product_name", with: p2.name fill_in "product_name", :with => "new product1" - click_on 'Update' - page.find("span#update-status-message").should have_content "Update complete" - end - - scenario "updating a product when there are more products than the default API page size" do - 26.times { FactoryGirl.create(:simple_product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - field = page.all("table#listing_products input[name='product_name']").first - field.set "new name" - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." + p1.reload + expect(p1.name).to eq "new product1" end describe "using action buttons" do @@ -586,15 +562,17 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.delete-product", :count => 3 + expect(page).to have_selector "a.delete-product", :count => 3 - first("a.delete-product").click + within "tr#p_#{p1.id}" do + first("a.delete-product").click + end - page.should have_selector "a.delete-product", :count => 2 + expect(page).to have_selector "a.delete-product", :count => 2 visit '/admin/products/bulk_edit' - page.should have_selector "a.delete-product", :count => 2 + expect(page).to have_selector "a.delete-product", :count => 2 end it "shows a delete button for variants, which deletes the appropriate variant when clicked" do @@ -604,20 +582,22 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "a.delete-variant", :count => 3 + expect(page).to have_selector "a.delete-variant", :count => 3 - first("a.delete-variant").click + within "tr#v_#{v1.id}" do + first("a.delete-variant").click + end - page.should have_selector "a.delete-variant", :count => 2 + expect(page).to have_selector "a.delete-variant", :count => 2 visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").select { |e| e.visible? }.each { |e| e.trigger('click') } - page.should have_selector "a.delete-variant", :count => 2 + expect(page).to have_selector "a.delete-variant", :count => 2 end end @@ -630,11 +610,13 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.edit-product", :count => 3 + expect(page).to have_selector "a.edit-product", :count => 3 - first("a.edit-product").click + within "tr#p_#{p1.id}" do + first("a.edit-product").click + end - URI.parse(current_url).path.should == "/admin/products/#{p1.permalink}/edit" + expect(URI.parse(current_url).path).to eq "/admin/products/#{p1.permalink}/edit" end it "shows an edit button for variants, which takes the user to the standard edit page for that variant" do @@ -644,14 +626,16 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - page.should have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants" all("a.view-variants").each { |e| e.trigger('click') } - page.should have_selector "a.edit-variant", :count => 3 + expect(page).to have_selector "a.edit-variant", :count => 3 - first("a.edit-variant").click + within "tr#v_#{v1.id}" do + first("a.edit-variant").click + end - URI.parse(current_url).path.should == "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" + expect(URI.parse(current_url).path).to eq "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" end end @@ -664,239 +648,110 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_selector "a.clone-product", :count => 3 + expect(page).to have_selector "a.clone-product", :count => 3 - first("a.clone-product").click - page.should have_selector "a.clone-product", :count => 4 - page.should have_field "product_name", with: "COPY OF #{p1.name}" - page.should have_select "supplier", selected: "#{p1.supplier.name}" + within "tr#p_#{p1.id}" do + first("a.clone-product").click + end + expect(page).to have_selector "a.clone-product", :count => 4 + expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" + expect(page).to have_select "producer", selected: "#{p1.supplier.name}" visit '/admin/products/bulk_edit' - page.should have_selector "a.clone-product", :count => 4 - page.should have_field "product_name", with: "COPY OF #{p1.name}" - page.should have_select "supplier", selected: "#{p1.supplier.name}" + expect(page).to have_selector "a.clone-product", :count => 4 + expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" + expect(page).to have_select "producer", selected: "#{p1.supplier.name}" end end end describe "using the page" do - describe "using tabs to hide and display page controls" do - it "shows a column display toggle button, which shows a list of columns when clicked" do - FactoryGirl.create(:simple_product) - login_to_admin_section - - visit '/admin/products/bulk_edit' - - page.should have_selector "div.column_toggle", :visible => false - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - first("div.option_tab_titles h6", :text => "Toggle Columns").click - - page.should have_selector "div.option_tab_titles h6.selected", :text => "Toggle Columns" - page.should have_selector "div.column_toggle", :visible => true - page.should have_selector "li.column-list-item", text: "Available On" - - page.should have_selector "div.filters", :visible => false - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - page.should have_selector "div.option_tab_titles h6.selected", :text => "Filter Products" - page.should have_selector "div.filters", :visible => true - - first("div.option_tab_titles h6", :text => "Filter Products").click - - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Filter Products" - page.should have_selector "div.option_tab_titles h6.unselected", :text => "Toggle Columns" - page.should have_selector "div.filters", :visible => false - page.should have_selector "div.column_toggle", :visible => false - end - end - - describe "using column display toggle" do - it "shows a column display toggle button, which shows a list of columns when clicked" do + describe "using column display dropdown" do + it "shows a column display dropdown, which shows a list of columns when clicked" do FactoryGirl.create(:simple_product) login_to_admin_section visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_selector "th", :text => "NAME" - page.should have_selector "th", :text => "SUPPLIER" - page.should have_selector "th", :text => "PRICE" - page.should have_selector "th", :text => "ON HAND" - page.should have_selector "th", :text => "AV. ON" + expect(page).to have_selector "th", :text => "NAME" + expect(page).to have_selector "th", :text => "PRODUCER" + expect(page).to have_selector "th", :text => "PRICE" + expect(page).to have_selector "th", :text => "ON HAND" + expect(page).to have_selector "th", :text => "AV. ON" - page.should have_selector "div.option_tab_titles h6", :text => "Toggle Columns" + first("div#columns_dropdown div.menu div.menu_item", text: /^.{0,1}Producer$/).click - page.should have_selector "div ul.column-list li.column-list-item", text: "Supplier" - first("li.column-list-item", text: "Supplier").click - - page.should_not have_selector "th", :text => "SUPPLIER" - page.should have_selector "th", :text => "NAME" - page.should have_selector "th", :text => "PRICE" - page.should have_selector "th", :text => "ON HAND" - page.should have_selector "th", :text => "AV. ON" - end - end - - describe "using pagination controls" do - it "shows pagination controls" do - 27.times { FactoryGirl.create(:product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - page.should have_select 'perPage', :selected => '25' - within '.pagination' do - page.should have_text "1 2" - page.should have_text "Next" - page.should have_text "Last" - end - end - - it "allows the number of visible products to be altered" do - 27.times { FactoryGirl.create(:product) } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.length.should == 25 - select '50', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.length.should == 27 - end - - it "displays the correct products when changing pages" do - 25.times { FactoryGirl.create(:product, :name => "page1product") } - 5.times { FactoryGirl.create(:product, :name => "page2product") } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page1product" }.should == true - find("a", text: "2").click - page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page2product" }.should == true - end - - it "moves the user to the last available page when changing the number of pages in any way causes user to become orphaned" do - 50.times { FactoryGirl.create(:product) } - FactoryGirl.create(:product, :name => "fancy_product_name") - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - find("a", text: "3").click - select '50', :from => 'perPage' - page.first("div.pagenav span.page.current").should have_text "2" - page.all("input[name='product_name']", :visible => true).length.should == 1 - - select '25', :from => 'perPage' - fill_in "quick_filter", :with => "fancy_product_name" - page.first("div.pagenav span.page.current").should have_text "1" - page.all("input[name='product_name']", :visible => true).length.should == 1 - end - - it "paginates the filtered product list rather than all products" do - 25.times { FactoryGirl.create(:product, :name => "product_name") } - 3.times { FactoryGirl.create(:product, :name => "test_product_name") } - login_to_admin_section - - visit '/admin/products/bulk_edit' - - select '25', :from => 'perPage' - page.should have_text "1 2" - fill_in "quick_filter", :with => "test_product_name" - page.all("input[name='product_name']", :visible => true).length.should == 3 - page.all("input[name='product_name']", :visible => true).all?{ |e| e.value == "test_product_name" }.should == true - page.should_not have_text "1 2" - page.should have_text "1" + expect(page).to have_no_selector "th", :text => "PRODUCER" + expect(page).to have_selector "th", :text => "NAME" + expect(page).to have_selector "th", :text => "PRICE" + expect(page).to have_selector "th", :text => "ON HAND" + expect(page).to have_selector "th", :text => "AV. ON" end end describe "using filtering controls" do - it "displays basic filtering controls" do - FactoryGirl.create(:simple_product) - + it "displays basic filtering controls which filter the product list" do + s1 = create(:supplier_enterprise) + s2 = create(:supplier_enterprise) + p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section + visit '/admin/products/bulk_edit' - page.should have_selector "div.option_tab_titles h6", :text => "Filter Products" - first("div.option_tab_titles h6", :text => "Filter Products").click + # Page shows the filter controls + expect(page).to have_select "producer_filter", visible: false + expect(page).to have_select "category_filter", visible: false - page.should have_select "filter_property", visible: false - page.should have_select "filter_predicate", visible: false - page.should have_field "filter_value" - end + # All products are shown when no filter is selected + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_field "product_name", with: p2.name - describe "clicking the 'Apply Filter' Button" do - before(:each) do - FactoryGirl.create(:simple_product, :name => "Product1") - FactoryGirl.create(:simple_product, :name => "Product2") + # Set a filter + select2_select s1.name, from: "producer_filter" - login_to_admin_section - visit '/admin/products/bulk_edit' + # Products are hidden when filtered out + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_no_field "product_name", with: p2.name - first("div.option_tab_titles h6", :text => "Filter Products").click + # Clearing filters + click_button "Clear Filters" - select2_select "Name", :from => "filter_property" - select2_select "Equals", :from => "filter_predicate" - fill_in "filter_value", :with => "Product1" - click_button "Apply Filter" - end - - it "adds a new filter to the list of applied filters" do - page.should have_text "Name Equals Product1" - end - - it "displays the 'loading' splash" do - page.should have_selector "div.loading", :text => "Loading Products..." - end - - it "loads appropriate products" do - page.should have_field "product_name", :with => "Product1" - page.should_not have_field "product_name", :with => "Product2" - end - - describe "clicking the 'Remove Filter' link" do - before(:each) do - find("a", text: "Remove Filter").click - end - - it "removes the filter from the list of applied filters" do - page.should_not have_text "Name Equals Product1" - end - - it "loads appropriate products" do - page.should have_field "product_name", :with => "Product1" - page.should have_field "product_name", :with => "Product2" - end - end + # All products are shown again + expect(page).to have_field "product_name", with: p1.name + expect(page).to have_field "product_name", with: p2.name end end end context "as an enterprise manager" do - let(:s1) { create(:supplier_enterprise, name: 'First Supplier') } - let(:s2) { create(:supplier_enterprise, name: 'Another Supplier') } - let(:s3) { create(:supplier_enterprise, name: 'Yet Another Supplier') } - let(:d1) { create(:distributor_enterprise, name: 'First Distributor') } - let(:d2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:product_supplied) { create(:product, supplier: s1, price: 10.0, on_hand: 6) } - let!(:product_not_supplied) { create(:product, supplier: s3) } - let(:product_supplied_inactive) { create(:product, supplier: s1, price: 10.0, on_hand: 6, available_on: 1.week.from_now) } + let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') } + let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') } + let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') } + let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') } + let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') } + let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') } + let!(:product_supplied) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6) } + let!(:product_not_supplied) { create(:product, supplier: supplier_unmanaged) } + let!(:product_supplied_permitted) { create(:product, name: 'Product Permitted', supplier: supplier_permitted, price: 10.0, on_hand: 6) } + let(:product_supplied_inactive) { create(:product, supplier: supplier_managed1, price: 10.0, on_hand: 6, available_on: 1.week.from_now) } - before(:each) do + let!(:supplier_permitted_relationship) do + create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1, + permissions_list: [:manage_products]) + end + + use_short_wait + + before do @enterprise_user = create_enterprise_user - @enterprise_user.enterprise_roles.build(enterprise: s1).save - @enterprise_user.enterprise_roles.build(enterprise: s2).save - @enterprise_user.enterprise_roles.build(enterprise: d1).save + @enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save + @enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save + @enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save login_to_admin_as @enterprise_user end @@ -904,15 +759,16 @@ feature %q{ it "shows only products that I supply" do visit '/admin/products/bulk_edit' - page.should have_field 'product_name', with: product_supplied.name - page.should_not have_field 'product_name', with: product_not_supplied.name + expect(page).to have_field 'product_name', with: product_supplied.name + expect(page).to have_field 'product_name', with: product_supplied_permitted.name + expect(page).to have_no_field 'product_name', with: product_not_supplied.name end - it "shows only suppliers that I manage" do + it "shows only suppliers that I manage or have permission to" do visit '/admin/products/bulk_edit' - page.should have_select 'supplier', with_options: [s1.name, s2.name], selected: s1.name - page.should_not have_select 'supplier', with_options: [s3.name] + expect(page).to have_select 'producer', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name + expect(page).to have_no_select 'producer', with_options: [supplier_unmanaged.name] end it "shows inactive products that I supply" do @@ -920,40 +776,44 @@ feature %q{ visit '/admin/products/bulk_edit' - page.should have_field 'product_name', with: product_supplied_inactive.name + expect(page).to have_field 'product_name', with: product_supplied_inactive.name end it "allows me to update a product" do - p = product_supplied + p = product_supplied_permitted visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click - page.should have_field "product_name", with: p.name - page.should have_select "supplier", selected: s1.name - page.should have_field "available_on", with: p.available_on.strftime("%F %T") - page.should have_field "price", with: "10.0" - page.should have_field "on_hand", with: "6" + within "tr#p_#{p.id}" do + expect(page).to have_field "product_name", with: p.name + expect(page).to have_select "producer", selected: supplier_permitted.name + expect(page).to have_field "available_on", with: p.available_on.strftime("%F %T") + expect(page).to have_field "price", with: "10.0" + expect(page).to have_field "on_hand", with: "6" - fill_in "product_name", with: "Big Bag Of Potatoes" - select s2.name, from: 'supplier' - fill_in "available_on", with: (Date.today-3).strftime("%F %T") - fill_in "price", with: "20" - fill_in "on_hand", with: "18" + fill_in "product_name", with: "Big Bag Of Potatoes" + select(supplier_managed2.name, :from => 'producer') + fill_in "available_on", with: (Date.today-3).strftime("%F %T") + fill_in "price", with: "20" + select "Weight (kg)", from: "variant_unit_with_scale" + fill_in "on_hand", with: "18" + fill_in "display_as", with: "Big Bag" + end - click_button 'Update' - page.find("span#update-status-message").should have_content "Update complete" + click_button 'Save Changes' + expect(page.find("#update-status-message")).to have_content "Changes saved." - visit '/admin/products/bulk_edit' - first("div.option_tab_titles h6", :text => "Toggle Columns").click - first("li.column-list-item", text: "Available On").click - - page.should have_field "product_name", with: "Big Bag Of Potatoes" - page.should have_select "supplier", selected: s2.name - page.should have_field "available_on", with: (Date.today-3).strftime("%F %T") - page.should have_field "price", with: "20.0" - page.should have_field "on_hand", with: "18" + p.reload + expect(p.name).to eq "Big Bag Of Potatoes" + expect(p.supplier).to eq supplier_managed2 + expect(p.variant_unit).to eq "weight" + expect(p.variant_unit_scale).to eq 1000 # Kg + expect(p.available_on).to eq 3.days.ago.beginning_of_day + expect(p.master.display_as).to eq "Big Bag" + expect(p.price).to eq 20.0 + expect(p.on_hand).to eq 18 end end end diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index 1443cd4c34..7317f44066 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -14,8 +14,9 @@ feature %q{ scenario "listing relationships" do # Given some enterprises with relationships e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) - create(:enterprise_relationship, parent: e1, child: e2) - create(:enterprise_relationship, parent: e3, child: e4) + create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: e2, child: e3, permissions_list: [:manage_products]) + create(:enterprise_relationship, parent: e3, child: e4, permissions_list: [:add_to_order_cycle, :manage_products]) # When I go to the relationships page click_link 'Enterprises' @@ -23,8 +24,10 @@ feature %q{ # Then I should see the relationships within('table#enterprise-relationships') do - page.should have_relationship e1, e2 - page.should have_relationship e3, e4 + page.should have_relationship e1, e2, ['can add to order cycle'] + page.should have_relationship e2, e3, ['can manage the products of'] + page.should have_relationship e3, e4, + ['can add to order cycle', 'can manage the products of'] end end @@ -35,11 +38,17 @@ feature %q{ visit admin_enterprise_relationships_path select 'One', from: 'enterprise_relationship_parent_id' + + check 'can add to order cycle' + check 'can manage the products of' + uncheck 'can manage the products of' select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2 - EnterpriseRelationship.where(parent_id: e1, child_id: e2).should be_present + page.should have_relationship e1, e2, ['can add to order cycle'] + er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first + er.should be_present + er.permissions.map(&:name).should == ['add_to_order_cycle'] end @@ -108,7 +117,9 @@ feature %q{ private - def have_relationship(parent, child) - have_table_row [parent.name, 'permits', child.name, ''] + def have_relationship(parent, child, perms=[]) + perms = perms.join(' ') || 'permits' + + have_table_row [child.name, perms, parent.name, ''] end end diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb new file mode 100644 index 0000000000..0e44c9cfe3 --- /dev/null +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +feature %q{ + As an Administrator + I want to manage relationships between users and enterprises +}, js: true do + include AuthenticationWorkflow + include WebHelper + + + context "as a site administrator" do + before { login_to_admin_section } + + scenario "listing relationships" do + # Given some users and enterprises with relationships + u1, u2 = create(:user), create(:user) + e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise) + create(:enterprise_role, user: u1, enterprise: e1) + create(:enterprise_role, user: u1, enterprise: e2) + create(:enterprise_role, user: u2, enterprise: e3) + create(:enterprise_role, user: u2, enterprise: e4) + + # When I go to the roles page + click_link 'Users' + click_link 'Roles' + + # Then I should see the relationships + within('table#enterprise-roles') do + page.should have_relationship u1, e1 + page.should have_relationship u1, e2 + page.should have_relationship u2, e3 + page.should have_relationship u2, e4 + end + end + + scenario "creating a relationship" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + + visit admin_enterprise_roles_path + select 'u@example.com', from: 'enterprise_role_user_id' + select 'One', from: 'enterprise_role_enterprise_id' + click_button 'Create' + + page.should have_relationship u, e + EnterpriseRole.where(user_id: u, enterprise_id: e).should be_present + end + + scenario "attempting to create a relationship with invalid data" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + create(:enterprise_role, user: u, enterprise: e) + + expect do + # When I attempt to create a duplicate relationship + visit admin_enterprise_roles_path + select 'u@example.com', from: 'enterprise_role_user_id' + select 'One', from: 'enterprise_role_enterprise_id' + click_button 'Create' + + # Then I should see an error message + page.should have_content "That role is already present." + end.to change(EnterpriseRole, :count).by(0) + end + + scenario "deleting a relationship" do + u = create(:user, email: 'u@example.com') + e = create(:enterprise, name: 'One') + er = create(:enterprise_role, user: u, enterprise: e) + + visit admin_enterprise_roles_path + page.should have_relationship u, e + + first("a.delete-enterprise-role").click + + page.should_not have_relationship u, e + EnterpriseRole.where(id: er.id).should be_empty + end + end + + + private + + def have_relationship(user, enterprise) + have_table_row [user.email, 'manages', enterprise.name, ''] + end +end diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index bf96f9eda2..ed46a1e0c1 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -6,72 +6,68 @@ feature %q{ } do include AuthenticationWorkflow include WebHelper + include AdminHelper - before(:each) do - @new_user = create_enterprise_user - @supplier1 = create(:supplier_enterprise, name: 'Supplier 1') - @supplier2 = create(:supplier_enterprise, name: 'Supplier 2') - @distributor1 = create(:distributor_enterprise, name: 'Distributor 3') - @distributor2 = create(:distributor_enterprise, name: 'Distributor 4') - end + let!(:user) { create_enterprise_user } + let!(:supplier1) { create(:supplier_enterprise, name: 'Supplier 1') } + let!(:supplier2) { create(:supplier_enterprise, name: 'Supplier 2') } + let(:supplier_profile) { create(:supplier_enterprise, name: 'Supplier profile', type: 'profile') } + let!(:distributor1) { create(:distributor_enterprise, name: 'Distributor 3') } + let!(:distributor2) { create(:distributor_enterprise, name: 'Distributor 4') } + let(:distributor_profile) { create(:distributor_enterprise, name: 'Distributor profile', type: 'profile') } - context "creating an Enterprise User" do - context 'with no enterprises' do - scenario "assigning a user to an Enterprise" do + describe "creating an enterprise user" do + context "with no enterprises managed" do + it "assigns an enterprise to a user" do login_to_admin_section click_link 'Users' - click_link @new_user.email + click_link user.email click_link 'Edit' - check @supplier2.name + check supplier2.name click_button 'Update' - @new_user.enterprises.count.should == 1 - @new_user.enterprises.first.name.should == @supplier2.name + user.enterprises.count.should == 1 + user.enterprises.first.name.should == supplier2.name end - end - context 'with existing enterprises' do - - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - @new_user.enterprise_roles.build(enterprise: @distributor1).save + context "with existing enterprises managed" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + user.enterprise_roles.create!(enterprise: distributor1) end - scenario "removing and add enterprises for a user" do + it "can remove and add enterprise management for a user" do login_to_admin_section click_link 'Users' - click_link @new_user.email + click_link user.email click_link 'Edit' - uncheck @distributor1.name # remove - check @distributor2.name # add + uncheck distributor1.name # remove + check distributor2.name # add click_button 'Update' - @new_user.enterprises.count.should == 2 - @new_user.enterprises.should include(@supplier1) - @new_user.enterprises.should include(@distributor2) + user.enterprises.count.should == 2 + user.enterprises.should include supplier1 + user.enterprises.should include distributor2 end - end - end - context "Product management" do - - context 'products I supply' do - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - product1 = create(:product, name: 'Green eggs', supplier: @supplier1) - product2 = create(:product, name: 'Ham', supplier: @supplier2) - login_to_admin_as @new_user + describe "product management" do + describe "managing supplied products" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + product1 = create(:product, name: 'Green eggs', supplier: supplier1) + product2 = create(:product, name: 'Ham', supplier: supplier2) + login_to_admin_as user end - scenario "manage products that I supply" do - visit '/admin/products' + it "can manage products that I supply" do + visit spree.admin_products_path within '#listing_products' do page.should have_content 'Green eggs' @@ -79,23 +75,88 @@ feature %q{ end end end - end - context "System management lockdown" do + describe "with only a profile-level enterprise" do + before do + user.enterprise_roles.create! enterprise: supplier_profile + user.enterprise_roles.create! enterprise: distributor_profile + login_to_admin_as user + end - before(:each) do - @new_user.enterprise_roles.build(enterprise: @supplier1).save - login_to_admin_as @new_user + it "shows me only menu items for enterprise management" do + page.should have_admin_menu_item 'Dashboard' + page.should have_admin_menu_item 'Enterprises' + + ['Orders', 'Products', 'Reports', 'Configuration', 'Promotions', 'Users', 'Order Cycles'].each do |menu_item_name| + page.should_not have_admin_menu_item menu_item_name + end + end + + describe "dashboard" do + it "shows me enterprise management controls" do + within('#enterprises') do + page.should have_selector 'h3', text: 'My Enterprises' + page.should have_link 'CREATE NEW' + page.should have_link supplier_profile.name + page.should have_link 'MANAGE MY ENTERPRISES' + end + end + + it "does not show me product management controls" do + page.should_not have_selector '#products' + page.should_not have_selector '#order_cycles' + end + + it "does not show me enterprise product info, payment methods, shipping methods or enterprise fees" do + # Producer product info + page.should_not have_selector '.producers_tab span', text: 'Total Products' + page.should_not have_selector '.producers_tab span', text: 'Active Products' + page.should_not have_selector '.producers_tab span', text: 'Products in OCs' + + # Payment methods, shipping methods, enterprise fees + page.should_not have_selector '.hubs_tab span', text: 'Payment Methods' + page.should_not have_selector '.hubs_tab span', text: 'Shipping Methods' + page.should_not have_selector '.hubs_tab span', text: 'Enterprise Fees' + end + end + + it "shows me only profile options on the enterprise listing page" do + click_link 'Enterprises' + + within "tr.enterprise-#{supplier_profile.id}" do + page.should_not have_link 'Enterprise Fees' + end + + within "tr.enterprise-#{distributor_profile.id}" do + page.should_not have_link 'Payment Methods' + page.should_not have_link 'Shipping Methods' + page.should_not have_link 'Enterprise Fees' + end + end + + it "shows me only profile fields on the hub edit page" do + click_link distributor_profile.name + + page.should_not have_selector '#payment_methods' + page.should_not have_selector '#shipping_methods' + page.should_not have_selector '#enterprise_fees' + end + end + + describe "system management lockdown" do + before do + user.enterprise_roles.create!(enterprise: supplier1) + login_to_admin_as user end scenario "should not be able to see system configuration" do - visit '/admin/general_settings/edit' + visit spree.edit_admin_general_settings_path page.should have_content 'Unauthorized' end scenario "should not be able to see user management" do - visit '/admin/users' + visit spree.admin_users_path page.should have_content 'Unauthorized' end end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 4d277972e5..da32f92681 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -73,6 +73,7 @@ feature %q{ click_link 'New Enterprise' fill_in 'enterprise_name', :with => 'Eaterprises' + choose 'Full' fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' fill_in 'enterprise_long_description', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' fill_in 'enterprise_distributor_info', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' @@ -113,7 +114,7 @@ feature %q{ e2 = create(:enterprise) eg1 = create(:enterprise_group, name: 'eg1') eg2 = create(:enterprise_group, name: 'eg2') - payment_method = create(:payment_method, distributors: []) + payment_method = create(:payment_method, distributors: [e2]) shipping_method = create(:shipping_method, distributors: [e2]) enterprise_fee = create(:enterprise_fee, enterprise: @enterprise ) @@ -123,6 +124,7 @@ feature %q{ all("a", text:'Edit Profile').first.click fill_in 'enterprise_name', :with => 'Eaterprises' + choose 'Single' fill_in 'enterprise_description', :with => 'Connecting farmers and eaters' fill_in 'enterprise_long_description', :with => 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 2423a6dc05..febd966109 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -436,26 +436,35 @@ feature %q{ context "as an enterprise user" do - let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } - let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } - let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } - let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:distributor1_fee) { create(:enterprise_fee, enterprise: distributor1, name: 'First Distributor Fee') } - before(:each) do - product = create(:product, supplier: supplier1) - product.distributors << distributor1 - product.save! + let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } + let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } + let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } + let!(:distributor_managed) { create(:distributor_enterprise, name: 'Managed distributor') } + let!(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Unmanaged Distributor') } + let!(:distributor_permitted) { create(:distributor_enterprise, name: 'Permitted distributor') } + let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } + let!(:supplier_permitted_relationship) do + create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, + permissions_list: [:add_to_order_cycle]) + end + let!(:distributor_permitted_relationship) do + create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, + permissions_list: [:add_to_order_cycle]) + end + let!(:product_managed) { create(:product, supplier: supplier_managed) } + let!(:product_permitted) { create(:product, supplier: supplier_permitted) } + before do @new_user = create_enterprise_user - @new_user.enterprise_roles.build(enterprise: supplier1).save - @new_user.enterprise_roles.build(enterprise: distributor1).save + @new_user.enterprise_roles.build(enterprise: supplier_managed).save + @new_user.enterprise_roles.build(enterprise: distributor_managed).save login_to_admin_as @new_user end scenario "viewing a list of order cycles I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) click_link "Order Cycles" @@ -464,8 +473,8 @@ feature %q{ page.should_not have_content oc_for_other_user.name # The order cycle should not show enterprises that I don't manage - page.should_not have_selector 'td.suppliers', text: supplier2.name - page.should_not have_selector 'td.distributors', text: distributor2.name + page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name + page.should_not have_selector 'td.distributors', text: distributor_unmanaged.name end scenario "creating a new order cycle" do @@ -476,57 +485,84 @@ feature %q{ fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'First Supplier', from: 'new_supplier_id' + select 'Managed supplier', from: 'new_supplier_id' + click_button 'Add supplier' + select 'Permitted supplier', from: 'new_supplier_id' click_button 'Add supplier' - select 'First Distributor', from: 'order_cycle_coordinator_id' - click_button 'Add coordinator fee' - select 'First Distributor Fee', from: 'order_cycle_coordinator_fee_0_id' + select_incoming_variant supplier_managed, 0, product_managed.master + select_incoming_variant supplier_permitted, 1, product_permitted.master - select 'First Distributor', from: 'new_distributor_id' + select 'Managed distributor', from: 'order_cycle_coordinator_id' + click_button 'Add coordinator fee' + select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' + + select 'Managed distributor', from: 'new_distributor_id' + click_button 'Add distributor' + select 'Permitted distributor', from: 'new_distributor_id' click_button 'Add distributor' - # Should only have suppliers / distributors listed which the user can manage - within "#new_supplier_id" do - page.should_not have_content supplier2.name - end - within "#new_distributor_id" do - page.should_not have_content distributor2.name - end - within "#order_cycle_coordinator_id" do - page.should_not have_content distributor2.name - page.should_not have_content supplier1.name - page.should_not have_content supplier2.name + # Should only have suppliers / distributors listed which the user is managing or + # has E2E permission to add products to order cycles + page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] + page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] + + [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| + page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] end click_button 'Create' flash_message.should == "Your order cycle has been created." order_cycle = OrderCycle.find_by_name('My order cycle') - order_cycle.coordinator.should == distributor1 + order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort + order_cycle.coordinator.should == distributor_managed + order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort end - scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) + scenario "editing an order cycle does not affect exchanges we don't manage" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) - # I should not see exchanges for supplier2 or distributor2 - page.all('tr.supplier').count.should == 1 - page.all('tr.distributor').count.should == 1 + # I should not see exchanges for supplier_unmanaged or distributor_unmanaged + page.all('tr.supplier').count.should == 2 + page.all('tr.distributor').count.should == 2 # When I save, then those exchanges should remain click_button 'Update' page.should have_content "Your order cycle has been updated." oc.reload - oc.suppliers.sort.should == [supplier1, supplier2] - oc.coordinator.should == supplier1 - oc.distributors.sort.should == [distributor1, distributor2] + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort + oc.coordinator.should == supplier_managed + oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort end + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # When I remove all the exchanges and save + page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click + page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click + click_button 'Update' + + # Then the exchanges should be removed + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.should == [supplier_unmanaged] + oc.coordinator.should == supplier_managed + oc.distributors.should == [distributor_unmanaged] + end + + scenario "cloning an order cycle" do - oc = create(:simple_order_cycle) + oc = create(:simple_order_cycle, coordinator: distributor_managed) click_link "Order Cycles" first('a.clone-order-cycle').click @@ -539,4 +575,11 @@ feature %q{ end + + private + + def select_incoming_variant(supplier, exchange_no, variant) + page.find("table.exchanges tr.supplier-#{supplier.id} td.products input").click + check "order_cycle_incoming_exchange_#{exchange_no}_variants_#{variant.id}" + end end diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 6c10c37541..dd733a7009 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -119,7 +119,6 @@ feature %q{ before(:each) do @enterprise_user = create_enterprise_user @enterprise_user.enterprise_roles.build(enterprise: supplier1).save - @enterprise_user.enterprise_roles.build(enterprise: supplier1).save @enterprise_user.enterprise_roles.build(enterprise: coordinator1).save @enterprise_user.enterprise_roles.build(enterprise: distributor1).save diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 660a187b3d..36fe344930 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -21,7 +21,7 @@ feature %q{ fill_in 'payment_method_name', :with => 'Cheque payment method' - select @distributors[0].name, :from => 'payment_method_distributor_ids', visible: false + check "payment_method_distributor_ids_#{@distributors[0].id}" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' @@ -29,6 +29,28 @@ feature %q{ payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [@distributors[0]] end + + scenario "updating a payment method" do + pm = create(:payment_method, distributors: [@distributors[0]]) + login_to_admin_section + + visit spree.edit_admin_payment_method_path pm + + fill_in 'payment_method_name', :with => 'New PM Name' + + uncheck "payment_method_distributor_ids_#{@distributors[0].id}" + check "payment_method_distributor_ids_#{@distributors[1].id}" + check "payment_method_distributor_ids_#{@distributors[2].id}" + select2_select "PayPal Express", from: "payment_method_type" + click_button 'Update' + + expect(flash_message).to eq 'Payment Method has been successfully updated!' + + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.distributors).to include @distributors[1], @distributors[2] + expect(payment_method.distributors).not_to include @distributors[0] + expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" + end end context "as an enterprise user" do @@ -46,14 +68,18 @@ feature %q{ login_to_admin_as enterprise_user end - it "creates payment methods" do + it "I can get to the new enterprise page" do click_link 'Enterprises' within(".enterprise-#{distributor1.id}") { click_link 'Payment Methods' } click_link 'New Payment Method' + current_path.should == spree.new_admin_payment_method_path + end + it "creates payment methods" do + visit spree.new_admin_payment_method_path fill_in 'payment_method_name', :with => 'Cheque payment method' - select distributor1.name, :from => 'payment_method_distributor_ids' + check "payment_method_distributor_ids_#{distributor1.id}" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' diff --git a/spec/features/consumer/groups_spec.rb b/spec/features/consumer/groups_spec.rb index 7baa5a2807..b2db10d58c 100644 --- a/spec/features/consumer/groups_spec.rb +++ b/spec/features/consumer/groups_spec.rb @@ -17,5 +17,6 @@ feature 'Groups', js: true do page.should have_content enterprise.name open_enterprise_modal enterprise modal_should_be_open_for enterprise + page.should have_content "Herndon, Vic" end end diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 81ee44fdcd..2e756879a9 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -12,7 +12,8 @@ feature "As a consumer I want to check out my cart", js: true do let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - let(:user) { create_enterprise_user } + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } after { Warden.test_reset! } before do @@ -38,6 +39,19 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_login_modal end + it "populates user details once logged in" do + visit checkout_path + within("section[role='main']") { click_button "Log in" } + page.should have_login_modal + fill_in "Email", with: user.email + fill_in "Password", with: user.password + within(".login-modal") { click_button 'Log in' } + toggle_details + + page.should have_field 'First Name', with: 'Foo' + page.should have_field 'Last Name', with: 'Bar' + end + it "allows user to checkout as guest" do visit checkout_path checkout_as_guest diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 9c5d56e8aa..b26e924495 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -10,27 +10,29 @@ feature "As a consumer I want to check out my cart", js: true do let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - before do + ActionMailer::Base.deliveries.clear + add_enterprise_fee enterprise_fee set_order order add_product_to_cart end - it "shows the current distributor oncheckout" do - visit checkout_path + it "shows the current distributor on checkout" do + visit checkout_path page.should have_content distributor.name end describe "with shipping methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow") } - let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue") } + let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } before do - distributor.shipping_methods << sm1 - distributor.shipping_methods << sm2 + distributor.shipping_methods << sm1 + distributor.shipping_methods << sm2 end context "on the checkout page" do @@ -39,13 +41,22 @@ feature "As a consumer I want to check out my cart", js: true do checkout_as_guest end + it "shows a breakdown of the order price" do + toggle_shipping + choose sm2.name + + page.should have_selector 'orderdetails .cart-total', text: "$11.23" + page.should have_selector 'orderdetails .shipping', text: "$4.56" + page.should have_selector 'orderdetails .total', text: "$15.79" + end + it "shows all shipping methods, but doesn't show ship address when not needed" do toggle_shipping page.should have_content "Frogs" page.should have_content "Donkeys" end - context "When shipping method requires an address" do + context "when shipping method requires an address" do before do toggle_shipping choose sm1.name @@ -61,61 +72,30 @@ feature "As a consumer I want to check out my cart", js: true do let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } let!(:pm2) { create(:payment_method, distributors: [distributor]) } let!(:pm3) do - Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test').tap do |pm| - pm.distributors << distributor + Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test', distributor_ids: [distributor.id]).tap do |pm| pm.preferred_login = 'devnull-facilitator_api1.rohanmitchell.com' pm.preferred_password = '1406163716' pm.preferred_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AaTntNJ-AjvUJkWf4dgJIvcLsf1V' end end - before do - visit checkout_path - checkout_as_guest - toggle_payment - end - - it "shows all available payment methods" do - page.should have_content pm1.name - page.should have_content pm2.name - page.should have_content pm3.name - end - - describe "Purchasing" do - it "takes us to the order confirmation page when we submit a complete form" do - ActionMailer::Base.deliveries.clear - toggle_shipping - choose sm2.name + context "on the checkout page with payments open" do + before do + visit checkout_path + checkout_as_guest toggle_payment - choose pm1.name - toggle_details - within "#details" do - fill_in "First Name", with: "Will" - fill_in "Last Name", with: "Marshall" - fill_in "Email", with: "test@test.com" - fill_in "Phone", with: "0468363090" - end - toggle_billing - within "#billing" do - fill_in "Address", with: "123 Your Face" - select "Australia", from: "Country" - select "Victoria", from: "State" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - - end - place_order - page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 1 - email = ActionMailer::Base.deliveries.last - site_name = Spree::Config[:site_name] - email.subject.should include "#{site_name} Order Confirmation" end - context "with basic details filled" do - before do + it "shows all available payment methods" do + page.should have_content pm1.name + page.should have_content pm2.name + page.should have_content pm3.name + end + + describe "purchasing" do + it "takes us to the order confirmation page when we submit a complete form" do toggle_shipping - choose sm1.name + choose sm2.name toggle_payment choose pm1.name toggle_details @@ -127,56 +107,111 @@ feature "As a consumer I want to check out my cart", js: true do end toggle_billing within "#billing" do - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" select "Victoria", from: "State" - end - toggle_shipping - check "Shipping address same as billing address?" - end + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" - it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + end place_order page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 + email = ActionMailer::Base.deliveries.last + site_name = Spree::Config[:site_name] + email.subject.should include "#{site_name} Order Confirmation" end - context "with a credit card payment method" do - let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } - - it "takes us to the order confirmation page when submitted with a valid credit card" do + context "with basic details filled" do + before do + toggle_shipping + choose sm1.name toggle_payment - fill_in 'Card Number', with: "4111111111111111" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' + choose pm1.name + toggle_details + within "#details" do + fill_in "First Name", with: "Will" + fill_in "Last Name", with: "Marshall" + fill_in "Email", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + toggle_billing + within "#billing" do + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + fill_in "Address", with: "123 Your Face" + select "Australia", from: "Country" + select "Victoria", from: "State" + end + toggle_shipping + check "Shipping address same as billing address?" + end + it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do place_order page.should have_content "Your order has been processed successfully" - - # Order should have a payment with the correct amount - o = Spree::Order.complete.first - o.payments.first.amount.should == 10 end - it "shows the payment processing failed message when submitted with an invalid credit card" do - toggle_payment - fill_in 'Card Number', with: "9999999988887777" - select 'February', from: 'secrets.card_month' - select (Date.today.year+1).to_s, from: 'secrets.card_year' - fill_in 'Security Code', with: '123' + context "with a credit card payment method" do + let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") } - place_order - page.should have_content "Payment could not be processed, please check the details you entered" + it "takes us to the order confirmation page when submitted with a valid credit card" do + toggle_payment + fill_in 'Card Number', with: "4111111111111111" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' - # Does not show duplicate shipping fee - visit checkout_path - page.all("th", text: "Shipping").count.should == 1 + place_order + page.should have_content "Your order has been processed successfully" + + # Order should have a payment with the correct amount + o = Spree::Order.complete.first + o.payments.first.amount.should == 11.23 + end + + it "shows the payment processing failed message when submitted with an invalid credit card" do + toggle_payment + fill_in 'Card Number', with: "9999999988887777" + select 'February', from: 'secrets.card_month' + select (Date.today.year+1).to_s, from: 'secrets.card_year' + fill_in 'Security Code', with: '123' + + place_order + page.should have_content "Payment could not be processed, please check the details you entered" + + # Does not show duplicate shipping fee + visit checkout_path + page.all("th", text: "Shipping").count.should == 1 + end end end end end + + context "when the customer has a pre-set shipping and billing address" do + before do + # Load up the customer's order and give them a shipping and billing address + # This is equivalent to when the customer has ordered before and their addresses + # are pre-populated. + o = Spree::Order.last + o.ship_address = build(:address) + o.bill_address = build(:address) + o.save! + end + + it "checks out successfully" do + visit checkout_path + checkout_as_guest + choose sm2.name + toggle_payment + choose pm1.name + + place_order + page.should have_content "Your order has been processed successfully" + ActionMailer::Base.deliveries.length.should == 2 + end + end end end end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 288e628b38..56c578f91b 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -139,7 +139,9 @@ feature "As a consumer I want to shop with a distributor", js: true do it "should save group buy data to ze cart" do fill_in "variants[#{product.master.id}]", with: 5 fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 9 - sleep 5 + + wait_until { !cart_dirty } + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 9 li.quantity.should == 5 diff --git a/spec/helpers/navigation_helper_spec.rb b/spec/helpers/navigation_helper_spec.rb new file mode 100644 index 0000000000..41decd6e5a --- /dev/null +++ b/spec/helpers/navigation_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +module Spree + module Admin + describe NavigationHelper do + describe "klass_for" do + it "returns the class when present" do + helper.klass_for('products').should == Spree::Product + end + + it "returns a symbol when there's no available class" do + helper.klass_for('reports').should == :report + end + + it "returns :overview for the dashboard" do + helper.klass_for('dashboard').should == :overview + end + end + end + end +end diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 44654e7b32..8c611081a2 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -4,10 +4,10 @@ //= require angular-sanitize //= require angular-mocks //= require angular-cookies -//= require angular-timer.min.js //= require angular-backstretch.js //= require lodash.underscore.js //= require angular-flash.min.js +//= require shared/mm-foundation-tpls-0.2.2.min.js //= require moment angular.module('templates', []) diff --git a/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee new file mode 100644 index 0000000000..2fcd776035 --- /dev/null +++ b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee @@ -0,0 +1,30 @@ +describe "ProvidersCtrl", -> + ctrl = null + scope = null + paymentMethod = null + + describe "initialising using a payment method without a type", -> + beforeEach -> + module 'ofn.admin' + scope = {} + paymentMethod = + type: null + + inject ($controller)-> + ctrl = $controller 'ProvidersCtrl', {$scope: scope, paymentMethod: paymentMethod } + + it "sets the invlude_html porperty on scope to blank", -> + expect(scope.include_html).toBe "" + + describe "initialising using a payment method with a type", -> + beforeEach -> + module 'ofn.admin' + scope = {} + paymentMethod = + type: "NOT NULL" + + inject ($controller)-> + ctrl = $controller 'ProvidersCtrl', {$scope: scope, paymentMethod: paymentMethod } + + it "sets the include_html porperty on scope to some address", -> + expect(scope.include_html).toBe "/admin/payment_methods/show_provider_preferences?provider_type=NOT NULL;pm_id=#{paymentMethod.id};" \ No newline at end of file diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee new file mode 100644 index 0000000000..9701377a73 --- /dev/null +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -0,0 +1,16 @@ +describe "enterprise relationships", -> + EnterpriseRelationships = null + enterprise_relationships = [] + + beforeEach -> + module "ofn.admin" + module ($provide) -> + $provide.value "enterprise_relationships", enterprise_relationships + null + + beforeEach inject (_EnterpriseRelationships_) -> + EnterpriseRelationships = _EnterpriseRelationships_ + + it "presents permission names", -> + expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "can add to order cycle" + expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "can manage the products of" diff --git a/spec/javascripts/unit/admin/services/taxons_spec.js.coffee b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee new file mode 100644 index 0000000000..9dd22935d5 --- /dev/null +++ b/spec/javascripts/unit/admin/services/taxons_spec.js.coffee @@ -0,0 +1,32 @@ +describe "Taxons service", -> + Taxons = taxons = $httpBackend = $resource = null + + beforeEach -> + module "ofn.admin" + module ($provide)-> + $provide.value "taxons", [{id: "1", name: "t1"}, {id: "2", name: "t2"}, {id: "12", name: "t12"}, {id: "31", name: "t31"}] + null + + beforeEach inject (_Taxons_, _$resource_, _$httpBackend_) -> + Taxons = _Taxons_ + $resource = _$resource_ + $httpBackend = _$httpBackend_ + + describe "findByID", -> + it "returns the taxon with exactly matching id, ignoring ids which do not exactly match", -> + result = Taxons.findByID("1") + expect(result).toEqual {id: "1", name: "t1"} + + describe "findByIDs", -> + it "returns taxons with exactly matching ids", -> + result = Taxons.findByIDs("1,2") + expect(result).toEqual [{id: "1", name: "t1"}, {id: "2", name: "t2"}] + + it "ignores ids which do not exactly match", -> + result = Taxons.findByIDs("1,3") + expect(result).toEqual [{id: "1", name: "t1"}] + + describe "findByTerm", -> + it "returns taxons which match partially", -> + result = Taxons.findByTerm("t1") + expect(result).toEqual [{id: "1", name: "t1"}, {id: "12", name: "t12"}] \ No newline at end of file diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 44466b5d10..b9081a1882 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -184,10 +184,7 @@ describe "filtering products for submission to database", -> created_at: null updated_at: null count_on_hand: 0 - supplier_id: 5 - supplier: - id: 5 - name: "Supplier 1" + producer: 5 group_buy: null group_buy_unit_size: null @@ -238,6 +235,11 @@ describe "AdminProductEditCtrl", -> beforeEach -> module "ofn.admin" + module ($provide)-> + $provide.value "producers", [] + $provide.value "taxons", [] + null + beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _DirtyProducts_) -> $scope = $rootScope.$new() $ctrl = _$controller_ @@ -249,38 +251,43 @@ describe "AdminProductEditCtrl", -> ) describe "loading data upon initialisation", -> - it "gets a list of suppliers and then resets products with a list of data", -> + it "gets a list of producers and then resets products with a list of data", -> $httpBackend.expectGET("/api/users/authorise_api?token=api_key").respond success: "Use of API Authorised" - $httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").respond "list of suppliers" spyOn($scope, "fetchProducts").andReturn "nothing" $scope.initialise "api_key" $httpBackend.flush() - expect($scope.suppliers).toEqual "list of suppliers" expect($scope.fetchProducts.calls.length).toEqual 1 expect($scope.spree_api_key_ok).toEqual true describe "fetching products", -> it "makes a standard call to dataFetcher when no filters exist", -> - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" $scope.fetchProducts() it "calls resetProducts after data has been received", -> spyOn $scope, "resetProducts" - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: "list of products" } $scope.fetchProducts() $httpBackend.flush() expect($scope.resetProducts).toHaveBeenCalledWith "list of products" + it "calls makes more calls to dataFetcher if more pages exist", -> + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 } + $httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] } + $scope.fetchProducts() + $httpBackend.flush() + it "applies filters when they are present", -> filter = {property: $scope.filterableColumns[1], predicate:$scope.filterTypes[0], value:"Product1"} $scope.currentFilters.push filter # Don't use addFilter as that is not what we are testing expect($scope.currentFilters).toEqual [filter] - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;q[name_eq]=Product1;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products" $scope.fetchProducts() + $httpBackend.flush() it "sets the loading property to true before fetching products and unsets it when loading is complete", -> - $httpBackend.expectGET("/api/products/managed?template=bulk_index;page=1;per_page=500;").respond "list of products" + $httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products" $scope.fetchProducts() expect($scope.loading).toEqual true $httpBackend.flush() @@ -324,7 +331,6 @@ describe "AdminProductEditCtrl", -> describe "preparing products", -> beforeEach -> - spyOn $scope, "matchSupplier" spyOn $scope, "loadVariantUnit" it "initialises display properties for the product", -> @@ -333,12 +339,6 @@ describe "AdminProductEditCtrl", -> $scope.unpackProduct product expect($scope.displayProperties[123]).toEqual {showVariants: false} - it "calls matchSupplier for the product", -> - product = {id: 123} - $scope.displayProperties = {} - $scope.unpackProduct product - expect($scope.matchSupplier.calls.length).toEqual 1 - it "calls loadVariantUnit for the product", -> product = {id: 123} $scope.displayProperties = {} @@ -346,33 +346,6 @@ describe "AdminProductEditCtrl", -> expect($scope.loadVariantUnit.calls.length).toEqual 1 - describe "matching supplier", -> - it "changes the supplier of the product to the one which matches it from the suppliers list", -> - s1_s = - id: 1 - name: "S1" - - s2_s = - id: 2 - name: "S2" - - s1_p = - id: 1 - name: "S1" - - expect(s1_s is s1_p).not.toEqual true - $scope.suppliers = [ - s1_s - s2_s - ] - product = - id: 10 - supplier: s1_p - - $scope.matchSupplier product - expect(product.supplier is s1_s).toEqual true - - describe "loading variant unit", -> describe "setting product variant_unit_with_scale field", -> it "sets by combining variant_unit and variant_unit_scale", -> @@ -397,8 +370,9 @@ describe "AdminProductEditCtrl", -> $scope.loadVariantUnit product expect(product.variant_unit_with_scale).toEqual "items" - it "loads data for variants (inc. master)", -> - spyOn $scope, "loadVariantVariantUnit" + it "loads data for variants (incl. master)", -> + spyOn $scope, "loadVariantUnitValues" + spyOn $scope, "loadVariantUnitValue" product = variant_unit_scale: 1.0 @@ -406,15 +380,27 @@ describe "AdminProductEditCtrl", -> variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] $scope.loadVariantUnit product - expect($scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.variants[0] - expect($scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.master + expect($scope.loadVariantUnitValues).toHaveBeenCalledWith product + expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.master + + it "loads data for variants (excl. master)", -> + spyOn $scope, "loadVariantUnitValue" + + product = + variant_unit_scale: 1.0 + master: {id: 1, unit_value: 1, unit_description: '(one)'} + variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] + $scope.loadVariantUnitValues product + + expect($scope.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] + expect($scope.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master describe "setting variant unit_value_with_description", -> it "sets by combining unit_value and unit_description", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0]).toEqual id: 1 unit_value: 1 @@ -425,28 +411,28 @@ describe "AdminProductEditCtrl", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 1}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '1' it "uses unit_description when value is missing", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_description: 'Small'}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual 'Small' it "converts values from base value to chosen unit", -> product = variant_unit_scale: 1000.0 variants: [{id: 1, unit_value: 2500}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '2.5' it "displays a unit_value of zero", -> product = variant_unit_scale: 1.0 variants: [{id: 1, unit_value: 0}] - $scope.loadVariantVariantUnit product, product.variants[0] + $scope.loadVariantUnitValues product, product.variants[0] expect(product.variants[0].unit_value_with_description).toEqual '0' @@ -827,6 +813,8 @@ describe "AdminProductEditCtrl", -> it "runs displaySuccess() when post returns success", -> spyOn $scope, "displaySuccess" + spyOn $scope, "updateVariantLists" + spyOn DirtyProducts, "clear" $scope.products = [ { id: 1 @@ -851,104 +839,34 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() $timeout.flush() expect($scope.displaySuccess).toHaveBeenCalled() + expect(DirtyProducts.clear).toHaveBeenCalled() + expect($scope.updateVariantLists).toHaveBeenCalled() - it "runs displayFailure() when post return data does not match $scope.products", -> - spyOn $scope, "displayFailure" - $scope.products = "current list of products" - $httpBackend.expectPOST("/admin/products/bulk_update").respond 200, "returned list of products" - $scope.updateProducts "updated list of products" - $httpBackend.flush() - expect($scope.displayFailure).toHaveBeenCalled() - - it "runs displayFailure() when post returns error", -> + it "runs displayFailure() when post returns an error", -> spyOn $scope, "displayFailure" $scope.products = "updated list of products" - $httpBackend.expectPOST("/admin/products/bulk_update").respond 404, "updated list of products" + $httpBackend.expectPOST("/admin/products/bulk_update").respond 500, "updated list of products" $scope.updateProducts "updated list of products" $httpBackend.flush() expect($scope.displayFailure).toHaveBeenCalled() - - describe "copying new variant ids from server to client", -> - it "copies server ids to the client where the client id is negative", -> - clientProducts = [ - { - id: 123 - variants: [{id: 1}, {id: -2}, {id: -3}] - } - ] - serverProducts = [ - { - id: 123 - variants: [{id: 1}, {id: 4534}, {id: 3453}] - } - ] - $scope.copyNewVariantIds(clientProducts, serverProducts) - expect(clientProducts).toEqual(serverProducts) - - - describe "fetching products without derived attributes", -> - it "returns products without the variant_unit_with_scale field", -> - $scope.products = [{id: 123, variant_unit_with_scale: 'weight_1000'}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual([{id: 123}]) - - it "returns an empty array when products are undefined", -> - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual([]) - - it "does not alter original products", -> - $scope.products = [{ - id: 123 - variant_unit_with_scale: 'weight_1000' - variants: [{options_text: 'foo'}] - }] - $scope.productsWithoutDerivedAttributes($scope.products) - expect($scope.products).toEqual [{ - id: 123 - variant_unit_with_scale: 'weight_1000' - variants: [{options_text: 'foo'}] - }] - - describe "updating variants", -> - it "returns variants without the unit_value_with_description field", -> - $scope.products = [{id: 123, variants: [{id: 234, unit_value_with_description: 'foo'}]}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual [ - { - id: 123 - variants: [{id: 234}] - } - ] - - it "removes the master variant", -> - $scope.products = [{id: 123, master: {id: 234, unit_value_with_description: 'foo'}}] - expect($scope.productsWithoutDerivedAttributes($scope.products)).toEqual [ - { - id: 123 - } - ] - - - describe "deep copying products", -> - it "copies products", -> - product = {id: 123} - copiedProducts = $scope.deepCopyProducts [product] - expect(copiedProducts[0]).not.toBe(product) - - it "copies variants", -> - variant = {id: 1} - product = {id: 123, variants: [variant]} - copiedProducts = $scope.deepCopyProducts [product] - expect(copiedProducts[0].variants[0]).not.toBe(variant) - + it "shows an alert with error information when post returns 400 with an errors array", -> + spyOn(window, "alert") + $scope.products = "updated list of products" + $httpBackend.expectPOST("/admin/products/bulk_update").respond 400, { "errors": ["an error"] } + $scope.updateProducts "updated list of products" + $httpBackend.flush() + expect(window.alert).toHaveBeenCalledWith("Saving failed with the following error(s):\nan error\n") describe "fetching a product by id", -> it "returns the product when it is present", -> product = {id: 123} $scope.products = [product] - expect($scope.findProduct(123)).toEqual product + expect($scope.findProduct(123, $scope.products)).toEqual product it "returns null when the product is not present", -> $scope.products = [] - expect($scope.findProduct(123)).toBeNull() + expect($scope.findProduct(123, $scope.products)).toBeNull() describe "adding variants", -> @@ -1122,7 +1040,7 @@ describe "AdminProductEditCtrl", -> $scope.cloneProduct $scope.products[0] $httpBackend.flush() - it "adds the newly created product to $scope.products and matches supplier", -> + it "adds the newly created product to $scope.products and matches producer", -> spyOn($scope, "unpackProduct").andCallThrough() $scope.products = [ id: 13 @@ -1132,8 +1050,7 @@ describe "AdminProductEditCtrl", -> product: id: 17 name: "new_product" - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1143,8 +1060,7 @@ describe "AdminProductEditCtrl", -> $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, id: 17 name: "new_product" - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1157,8 +1073,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1175,8 +1090,7 @@ describe "AdminProductEditCtrl", -> id: 17 name: "new_product" variant_unit_with_scale: null - supplier: - id: 6 + producer: 6 variants: [ id: 3 @@ -1188,94 +1102,15 @@ describe "AdminProductEditCtrl", -> describe "filtering products", -> - describe "adding a filter to the filter list", -> - filterObject1 = filterObject2 = null - - beforeEach -> - spyOn($scope, "fetchProducts").andReturn "nothing" - spyOn(DirtyProducts, "count").andReturn 0 - filterObject1 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "value1"} - filterObject2 = {property: $scope.filterableColumns[1], predicate: $scope.filterTypes[1], value: "value2"} - $scope.addFilter filterObject1 - $scope.addFilter filterObject2 - - it "adds objects sent to addFilter() to $scope.currentFilters", -> - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which do not contain a 'property' with a corresponding key in filterableColumns", -> - filterObject3 = {property: "some_random_property", predicate: $scope.filterTypes[0], value: "value3"} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which do not contain a query with a corresponding key in filterTypes", -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: "something", value: "value3"} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "ignores objects sent to addFilter() which have a blank 'value' property", -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[1], value: ""} - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "calls fetchProducts when adding a new filter", -> - expect($scope.fetchProducts.calls.length).toEqual(2) - - describe "when unsaved products exist", -> - beforeEach -> - filterObject3 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[1], value: "value3"} - spyOn(window, "confirm").andReturn false - DirtyProducts.count.andReturn 1 - $scope.addFilter filterObject3 - - it "it does not call fetchProducts", -> - expect($scope.fetchProducts.calls.length).toEqual(2) - - it "does not add the filter to $scope.currentFilters", -> - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - it "asks the user to save changes before proceeding", -> - expect(window.confirm).toHaveBeenCalledWith "Unsaved changes will be lost. Continue anyway?" - - describe "when a filter on the same property and predicate already exists", -> - filterObject3 = null - - beforeEach -> - filterObject3 = { property: filterObject2.property, predicate: filterObject2.predicate, value: "new value" } - - it "asks the user for permission before proceeding", -> - spyOn(window, "confirm").andReturn true - $scope.addFilter filterObject3 - expect(window.confirm).toHaveBeenCalledWith "'#{filterObject3.predicate.name}' filter already exists on column '#{filterObject3.property.name}'. Replace it?" - - it "replaces the filter in $scope.currentFilters when user clicks OK", -> - spyOn(window, "confirm").andReturn true - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject3] - - it "does not add the filter to $scope.currentFilters when user clicks cancel", -> - spyOn(window, "confirm").andReturn false - $scope.addFilter filterObject3 - expect($scope.currentFilters).toEqual [filterObject1, filterObject2] - - describe "removing a filter from the filter list", -> - filterObject1 = filterObject2 = null - - beforeEach -> - spyOn($scope, "fetchProducts").andReturn "nothing" - filterObject1 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "Product1"} - filterObject2 = {property: $scope.filterableColumns[0], predicate: $scope.filterTypes[0], value: "Product2"} - $scope.currentFilters = [ filterObject1, filterObject2 ] - - it "removes the specified filter from $scope.currentFilters and calls fetchProducts", -> - $scope.removeFilter filterObject1 - expect($scope.currentFilters).toEqual [ filterObject2 ] - expect($scope.fetchProducts.calls.length).toEqual 1 - - it "ignores filters which do not exist in currentFilters", -> - filterObject3 = {property: $scope.filterableColumns[1], predicate: $scope.filterTypes[1], value: "SomethingElse"} - $scope.removeFilter filterObject3 - expect($scope.currentFilters).toEqual [ filterObject1, filterObject2 ] - expect($scope.fetchProducts.calls.length).toEqual 0 + describe "clearing filters", -> + it "resets filter variables", -> + $scope.query = "lala" + $scope.producerFilter = "5" + $scope.categoryFilter = "6" + $scope.resetSelectFilters() + expect($scope.query).toBe "" + expect($scope.producerFilter).toBe "0" + expect($scope.categoryFilter).toBe "0" describe "converting arrays of objects with ids to an object with ids as keys", -> @@ -1334,28 +1169,3 @@ describe "converting arrays of objects with ids to an object with ids as keys", expect(toObjectWithIDKeys).toHaveBeenCalledWith [id: 17] expect(toObjectWithIDKeys).not.toHaveBeenCalledWith {12: {id: 12}} - -describe "Taxons service", -> - Taxons = $httpBackend = $resource = null - - beforeEach -> - module "ofn.admin" - - beforeEach inject (_Taxons_, _$resource_, _$httpBackend_) -> - Taxons = _Taxons_ - $resource = _$resource_ - $httpBackend = _$httpBackend_ - - it "calling findByIDs makes a http request", -> - response = { taxons: "list of taxons by id" } - $httpBackend.expectGET("/admin/taxons/search?ids=1,2").respond 200, response - taxons = Taxons.findByIDs("1,2") - $httpBackend.flush() - expect(angular.equals(taxons,response)).toBe true - - it "calling findByTerm makes a http request", -> - response = { taxons: "list of taxons by term" } - $httpBackend.expectGET("/admin/taxons/search?q=lala").respond 200, response - taxons = Taxons.findByTerm("lala") - $httpBackend.flush() - expect(angular.equals(taxons,response)).toBe true diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index ceebc55eab..fdbc4742a3 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -46,13 +46,13 @@ describe "CheckoutCtrl", -> describe "Local storage", -> it "binds to localStorage when given a scope", -> - prefix = "order_#{scope.order.id}#{scope.order.user_id}#{CurrentHubMock.hub.id}" + prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" field = scope.fieldsToBind[0] expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.order.#{field}", {storeName: "#{prefix}_#{field}"}) expect(storage.bind).toHaveBeenCalledWith(scope, "Checkout.ship_address_same_as_billing", {storeName: "#{prefix}_sameasbilling", defaultValue: true}) it "it can retrieve data from localstorage", -> - prefix = "order_#{scope.order.id}#{scope.order.user_id}#{CurrentHubMock.hub.id}" + prefix = "order_#{scope.order.id}#{CurrentUser?.id}#{CurrentHubMock.hub.id}" expect(localStorage.getItem("#{prefix}_email")).toMatch "public" it "does not store secrets in local storage", -> diff --git a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee index 1d56a79d27..73ecd611ef 100644 --- a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee @@ -3,6 +3,7 @@ describe 'ProductsCtrl', -> scope = null event = null Products = null + Cart = {} beforeEach -> module('Darkswarm') @@ -15,7 +16,7 @@ describe 'ProductsCtrl', -> inject ($controller) -> scope = {} - ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle} + ctrl = $controller 'ProductsCtrl', {$scope: scope, Products: Products, OrderCycle: OrderCycle, Cart: Cart} it 'fetches products from Products', -> expect(scope.Products.products).toEqual ['testy mctest'] diff --git a/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee index e7e2614f7f..0e85fe27d3 100644 --- a/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/filters/filter_groups_spec.js.coffee @@ -2,7 +2,7 @@ describe "filtering Groups", -> filterGroups = null groups = [{ name: "test" - long_description: "roger" + description: "roger" enterprises: [{ name: "kittens" }, { @@ -10,7 +10,7 @@ describe "filtering Groups", -> }] }, { name: "blankness" - long_description: "in the sky" + description: "in the sky" enterprises: [{ name: "ponies" }, { diff --git a/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee index f34df5cc71..ae0d6a7c8a 100644 --- a/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee @@ -6,11 +6,8 @@ describe 'filtering urls', -> inject ($filter) -> filter = $filter('stripUrl') - it "removes http and www", -> - expect(filter("http://www.footle.com")).toEqual "footle.com" + it "removes http", -> + expect(filter("http://footle.com")).toEqual "footle.com" - it "removes https and www", -> - expect(filter("https://www.footle.com")).toEqual "footle.com" - - it "removes just www", -> - expect(filter("www.footle.com")).toEqual "footle.com" + it "removes https", -> + expect(filter("https://www.footle.com")).toEqual "www.footle.com" diff --git a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee index af384adcb6..27e3f5aff6 100644 --- a/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee @@ -7,6 +7,7 @@ describe 'Products service', -> CurrentHubMock = {} currentOrder = null product = null + productWithImage = null beforeEach -> product = @@ -14,7 +15,16 @@ describe 'Products service', -> supplier: id: 9 price: 11 + master: {} variants: [] + productWithImage = + supplier: + id: 9 + master: {} + variants: [] + images: [ + large_url: 'foo.png' + ] currentOrder = line_items: [] @@ -39,7 +49,7 @@ describe 'Products service', -> it "dereferences suppliers", -> Enterprises.enterprises_by_id = {id: 9, name: "test"} - $httpBackend.expectGET("/shop/products").respond([{supplier : {id: 9}}]) + $httpBackend.expectGET("/shop/products").respond([{supplier : {id: 9}, master: {}}]) $httpBackend.flush() expect(Products.products[0].supplier).toBe Enterprises.enterprises_by_id["9"] @@ -55,6 +65,17 @@ describe 'Products service', -> $httpBackend.flush() expect(Cart.line_items[0].variant).toBe Products.products[0].variants[0] + it "sets primaryImageOrMissing when no images are provided", -> + $httpBackend.expectGET("/shop/products").respond([product]) + $httpBackend.flush() + expect(Products.products[0].primaryImage).toBeUndefined() + expect(Products.products[0].primaryImageOrMissing).toEqual "/assets/noimage/small.png" + + it "sets largeImage", -> + $httpBackend.expectGET("/shop/products").respond([productWithImage]) + $httpBackend.flush() + expect(Products.products[0].largeImage).toEqual("foo.png") + describe "determining the price to display for a product", -> it "displays the product price when the product does not have variants", -> $httpBackend.expectGET("/shop/products").respond([product]) @@ -66,4 +87,3 @@ describe 'Products service', -> $httpBackend.expectGET("/shop/products").respond([product]) $httpBackend.flush() expect(Products.products[0].price).toEqual 22 - diff --git a/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee new file mode 100644 index 0000000000..ebfba165f6 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/shipping_methods_spec.js.coffee @@ -0,0 +1,14 @@ +describe "Shipping method service", -> + ShippingMethods = null + shippingMethods = [ + {id: 1, price: "1.2"} + ] + + beforeEach -> + module 'Darkswarm' + angular.module('Darkswarm').value('shippingMethods', shippingMethods) + inject ($injector)-> + ShippingMethods = $injector.get("ShippingMethods") + + it "converts price to float", -> + expect(ShippingMethods.shipping_methods[0].price).toEqual 1.2 diff --git a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee index 278e0a6850..ac9865a142 100644 --- a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee @@ -5,6 +5,8 @@ describe 'Variants service', -> beforeEach -> variant = id: 1 + base_price: 80.5 + price: 100 module 'Darkswarm' inject ($injector)-> Variants = $injector.get("Variants") @@ -19,3 +21,5 @@ describe 'Variants service', -> it "will return the same object as passed", -> expect(Variants.register(variant)).toBe variant + it "initialises base price percentage", -> + expect(Variants.register(variant).basePricePercentage).toEqual 81 diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 172b2cdb1a..0d5127314a 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -327,7 +327,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> Enterprise = $injector.get('Enterprise') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprises.json').respond [ + $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json').respond [ {id: 1, name: 'One', supplied_products: [1, 2]} {id: 2, name: 'Two', supplied_products: [3, 4]} {id: 3, name: 'Three', supplied_products: [5, 6]} diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb index d10a5e9b72..9ad1d222b3 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -27,7 +27,6 @@ module OpenFoodNetwork it "creates an adjustment for an order" do order = create(:order) - #line_item = create(:line_item) enterprise_fee = create(:enterprise_fee) product = create(:simple_product) diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb new file mode 100644 index 0000000000..9c950234cd --- /dev/null +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -0,0 +1,160 @@ +require 'open_food_network/enterprise_fee_calculator' + +module OpenFoodNetwork + describe EnterpriseFeeCalculator do + describe "integration" do + let(:coordinator) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:product) { create(:simple_product, price: 10.00) } + + describe "calculating fees for a variant" do + it "sums all the per-item fees for the variant in the specified hub + order cycle" do + enterprise_fee1 = create(:enterprise_fee, amount: 20) + enterprise_fee2 = create(:enterprise_fee, amount: 3) + enterprise_fee3 = create(:enterprise_fee, + calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) + + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 23 + end + + it "sums percentage fees for the variant" do + enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1], variants: [product.master]) + + product.master.price.should == 10.00 + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 2.00 + end + end + + describe "calculating fees by type" do + let!(:ef_admin) { create(:enterprise_fee, fee_type: 'admin', amount: 1.23) } + let!(:ef_sales) { create(:enterprise_fee, fee_type: 'sales', amount: 4.56) } + let!(:ef_packing) { create(:enterprise_fee, fee_type: 'packing', amount: 7.89) } + let!(:ef_transport) { create(:enterprise_fee, fee_type: 'transport', amount: 0.12) } + let!(:ef_fundraising) { create(:enterprise_fee, fee_type: 'fundraising', amount: 3.45) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, + sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [ef_admin, ef_sales, ef_packing, ef_transport, ef_fundraising], + variants: [product.master]) } + + it "returns a breakdown of fees" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end + + it "filters out zero fees" do + ef_admin.calculator.update_attribute :preferred_amount, 0 + + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end + end + + describe "creating adjustments" do + let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } + let!(:line_item) { create(:line_item, order: order, variant: product.master) } + let(:enterprise_fee_line_item) { create(:enterprise_fee) } + let(:enterprise_fee_order) { create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, variants: [product.master]) } + + before { order.reload } + + it "creates adjustments for a line item" do + exchange.enterprise_fees << enterprise_fee_line_item + + EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_line_item.name + end + + it "creates adjustments for an order" do + exchange.enterprise_fees << enterprise_fee_order + + EnterpriseFeeCalculator.new.create_order_adjustments_for order + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_order.name + end + end + end + + describe "creating adjustments for a line item" do + let(:oc) { OrderCycle.new } + let(:variant) { double(:variant) } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + let(:line_item) { double(:line_item, variant: variant, order: order) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_line_item_adjustment).with(line_item) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } + + efc.create_line_item_adjustments_for line_item + end + + it "makes fee applicators for a line item" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } + + oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] + end + end + + describe "creating adjustments for an order" do + let(:oc) { OrderCycle.new } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_order_adjustment).with(order) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + + efc.create_order_adjustments_for order + end + + it "makes fee applicators for an order" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } + + oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_order_enterprise_fee_applicators_for, order).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + end + end + end +end diff --git a/spec/lib/open_food_network/option_value_namer_spec.rb b/spec/lib/open_food_network/option_value_namer_spec.rb index b16574ba4c..354fd46369 100644 --- a/spec/lib/open_food_network/option_value_namer_spec.rb +++ b/spec/lib/open_food_network/option_value_namer_spec.rb @@ -4,34 +4,34 @@ module OpenFoodNetwork describe OptionValueNamer do describe "generating option value name" do let(:v) { Spree::Variant.new } - let(:subject) { OptionValueNamer.new v } + let(:subject) { OptionValueNamer.new } it "when description is blank" do v.stub(:unit_description) { nil } subject.stub(:value_scaled?) { true } subject.stub(:option_value_value_unit) { %w(value unit) } - subject.name.should == "valueunit" + subject.name(v).should == "valueunit" end it "when description is present" do v.stub(:unit_description) { 'desc' } subject.stub(:option_value_value_unit) { %w(value unit) } subject.stub(:value_scaled?) { true } - subject.name.should == "valueunit desc" + subject.name(v).should == "valueunit desc" end it "when value is blank and description is present" do v.stub(:unit_description) { 'desc' } subject.stub(:option_value_value_unit) { [nil, nil] } subject.stub(:value_scaled?) { true } - subject.name.should == "desc" + subject.name(v).should == "desc" end it "spaces value and unit when value is unscaled" do v.stub(:unit_description) { nil } subject.stub(:option_value_value_unit) { %w(value unit) } subject.stub(:value_scaled?) { false } - subject.name.should == "value unit" + subject.name(v).should == "value unit" end end @@ -42,7 +42,7 @@ module OpenFoodNetwork v.stub(:product) { p } subject = OptionValueNamer.new v - subject.value_scaled?.should be_true + expect(subject.send(:value_scaled?)).to be_true end it "returns false otherwise" do @@ -51,7 +51,7 @@ module OpenFoodNetwork v.stub(:product) { p } subject = OptionValueNamer.new v - subject.value_scaled?.should be_false + expect(subject.send(:value_scaled?)).to be_false end end @@ -65,7 +65,7 @@ module OpenFoodNetwork v.stub(:unit_value) { 100 } - subject.option_value_value_unit.should == [100, 'g'] + expect(subject.send(:option_value_value_unit)).to eq [100, 'g'] end it "generates values when unit value is non-integer" do @@ -73,7 +73,7 @@ module OpenFoodNetwork v.stub(:product) { p } v.stub(:unit_value) { 123.45 } - subject.option_value_value_unit.should == [123.45, 'g'] + expect(subject.send(:option_value_value_unit)).to eq [123.45, 'g'] end it "returns a value of 1 when unit value equals the scale" do @@ -81,7 +81,7 @@ module OpenFoodNetwork v.stub(:product) { p } v.stub(:unit_value) { 1000.0 } - subject.option_value_value_unit.should == [1, 'kg'] + expect(subject.send(:option_value_value_unit)).to eq [1, 'kg'] end it "generates values for all weight scales" do @@ -89,7 +89,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'weight', variant_unit_scale: scale) v.stub(:product) { p } v.stub(:unit_value) { 100 * scale } - subject.option_value_value_unit.should == [100, unit] + expect(subject.send(:option_value_value_unit)).to eq [100, unit] end end @@ -98,7 +98,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'volume', variant_unit_scale: scale) v.stub(:product) { p } v.stub(:unit_value) { 100 * scale } - subject.option_value_value_unit.should == [100, unit] + expect(subject.send(:option_value_value_unit)).to eq [100, unit] end end @@ -106,7 +106,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'volume', variant_unit_scale: 0.001) v.stub(:product) { p } v.stub(:unit_value) { 0.0001 } - subject.option_value_value_unit.should == [0.1, 'mL'] + expect(subject.send(:option_value_value_unit)).to eq [0.1, 'mL'] end it "generates values for item units" do @@ -114,7 +114,7 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit) v.stub(:product) { p } v.stub(:unit_value) { 100 } - subject.option_value_value_unit.should == [100, unit.pluralize] + expect(subject.send(:option_value_value_unit)).to eq [100, unit.pluralize] end end @@ -122,14 +122,14 @@ module OpenFoodNetwork p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'packet') v.stub(:product) { p } v.stub(:unit_value) { 1 } - subject.option_value_value_unit.should == [1, 'packet'] + expect(subject.send(:option_value_value_unit)).to eq [1, 'packet'] end it "returns [nil, nil] when unit value is not set" do p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'foo') v.stub(:product) { p } v.stub(:unit_value) { nil } - subject.option_value_value_unit.should == [nil, nil] + expect(subject.send(:option_value_value_unit)).to eq [nil, nil] end end end diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb new file mode 100644 index 0000000000..bcd1f8b742 --- /dev/null +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -0,0 +1,132 @@ +require 'open_food_network/permissions' + +module OpenFoodNetwork + describe Permissions do + let(:user) { double(:user) } + let(:permissions) { Permissions.new(user) } + let(:permission) { 'one' } + let(:e1) { create(:enterprise) } + let(:e2) { create(:enterprise) } + + describe "finding enterprises that can be added to an order cycle" do + let(:e) { double(:enterprise) } + + it "returns managed and related enterprises with add_to_order_cycle permission" do + permissions. + should_receive(:managed_and_related_enterprises_with). + with(:add_to_order_cycle). + and_return([e]) + + permissions.order_cycle_enterprises.should == [e] + end + end + + describe "finding exchanges of an order cycle that an admin can manage" do + let(:oc) { create(:simple_order_cycle) } + let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } + + before do + permissions.stub(:managed_enterprises) { [] } + permissions.stub(:related_enterprises_with) { [] } + end + + it "returns exchanges involving enterprises managed by the user" do + permissions.stub(:managed_enterprises) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "returns exchanges involving enterprises with E2E permission" do + permissions.stub(:related_enterprises_with) { [e1, e2] } + permissions.order_cycle_exchanges(oc).should == [ex] + end + + it "does not return exchanges involving only the sender" do + permissions.stub(:managed_enterprises) { [e1] } + permissions.order_cycle_exchanges(oc).should == [] + end + + it "does not return exchanges involving only the receiver" do + permissions.stub(:managed_enterprises) { [e2] } + permissions.order_cycle_exchanges(oc).should == [] + end + end + + describe "finding managed products" do + let!(:p1) { create(:simple_product) } + let!(:p2) { create(:simple_product) } + + before do + permissions.stub(:managed_enterprise_products) { Spree::Product.where('1=0') } + permissions.stub(:related_enterprise_products) { Spree::Product.where('1=0') } + end + + it "returns products produced by managed enterprises" do + permissions.stub(:managed_enterprise_products) { Spree::Product.where(id: p1) } + permissions.managed_products.should == [p1] + end + + it "returns products produced by permitted enterprises" do + permissions.stub(:related_enterprise_products) { Spree::Product.where(id: p2) } + permissions.managed_products.should == [p2] + end + end + + describe "finding enterprises that we manage products for" do + let(:e) { double(:enterprise) } + + it "returns managed and related enterprises with manage_products permission" do + permissions. + should_receive(:managed_and_related_enterprises_with). + with(:manage_products). + and_return([e]) + + permissions.managed_product_enterprises.should == [e] + end + end + + ######################################## + + describe "finding related enterprises with a particular permission" do + let!(:er) { create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [permission]) } + + it "returns the enterprises" do + permissions.stub(:managed_enterprises) { e2 } + permissions.send(:related_enterprises_with, permission).should == [e1] + end + + it "returns an empty array when there are none" do + permissions.stub(:managed_enterprises) { e1 } + permissions.send(:related_enterprises_with, permission).should == [] + end + end + + describe "finding enterprises that are managed or with a particular permission" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } + permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } + end + + it "returns managed enterprises" do + permissions.should_receive(:managed_enterprises) { Enterprise.where(id: e1) } + permissions.send(:managed_and_related_enterprises_with, permission).should == [e1] + end + + it "returns permitted enterprises" do + permissions.should_receive(:related_enterprises_with).with(permission). + and_return(Enterprise.where(id: e2)) + permissions.send(:managed_and_related_enterprises_with, permission).should == [e2] + end + end + + describe "finding the supplied products of related enterprises" do + let!(:e) { create(:enterprise) } + let!(:p) { create(:simple_product, supplier: e) } + + it "returns supplied products" do + permissions.should_receive(:related_enterprises_with).with(:manage_products) { [e] } + + permissions.send(:related_enterprise_products).should == [p] + end + end + end +end diff --git a/spec/lib/open_food_network/searcher_spec.rb b/spec/lib/open_food_network/searcher_spec.rb deleted file mode 100644 index 2fb83ed309..0000000000 --- a/spec/lib/open_food_network/searcher_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' -require 'open_food_network/searcher' - -module OpenFoodNetwork - describe Searcher do - it "searches by supplier" do - # Given products under two suppliers - s1 = create(:supplier_enterprise) - s2 = create(:supplier_enterprise) - p1 = create(:product, :supplier => s1) - p2 = create(:product, :supplier => s2) - - # When we search for one supplier, we should see only products from that supplier - searcher = Searcher.new(:supplier_id => s1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - end - - it "searches by distributor" do - # Given products under two distributors - d1 = create(:distributor_enterprise) - d2 = create(:distributor_enterprise) - p1 = create(:product, :distributors => [d1]) - p2 = create(:product, :distributors => [d2]) - - # When we search for one distributor, we should see only products from that distributor - searcher = Searcher.new(:distributor_id => d1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - end - - it "searches by supplier or distributor" do - # Given products under some suppliers and distributors - s0 = create(:supplier_enterprise) - s1 = create(:supplier_enterprise) - d1 = create(:distributor_enterprise) - p1 = create(:product, :supplier => s1) - p2 = create(:product, :distributors => [d1]) - p3 = create(:product, :supplier => s0) - - # When we search by the supplier enterprise, we should see the supplied products - searcher = Searcher.new(:enterprise_id => s1.id.to_s) - products = searcher.retrieve_products - products.should == [p1] - - # When we search by the distributor enterprise, we should see the distributed products - searcher = Searcher.new(:enterprise_id => d1.id.to_s) - products = searcher.retrieve_products - products.should == [p2] - end - end -end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index b715674594..3a6c48e891 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -6,10 +6,10 @@ describe EnterpriseRelationship do let(:e2) { create(:enterprise, name: 'B') } let(:e3) { create(:enterprise, name: 'C') } - it "sorts by parent, child enterprise name" do - er1 = create(:enterprise_relationship, parent: e1, child: e3) - er2 = create(:enterprise_relationship, parent: e2, child: e1) - er3 = create(:enterprise_relationship, parent: e1, child: e2) + it "sorts by child, parent enterprise name" do + er1 = create(:enterprise_relationship, parent: e3, child: e1) + er2 = create(:enterprise_relationship, parent: e1, child: e2) + er3 = create(:enterprise_relationship, parent: e2, child: e1) EnterpriseRelationship.by_name.should == [er3, er1, er2] end @@ -29,5 +29,38 @@ describe EnterpriseRelationship do EnterpriseRelationship.involving_enterprises([e3]).should == [] end end + + describe "creating with a permission list" do + it "creates permissions with a list" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: ['one', 'two'] + er.reload + er.permissions.map(&:name).sort.should == ['one', 'two'].sort + end + + it "does nothing when the list is nil" do + er = EnterpriseRelationship.create! parent: e1, child: e2, permissions_list: nil + er.reload + er.permissions.should be_empty + end + end + + it "finds relationships that grant permissions to some enterprises" do + er1 = create(:enterprise_relationship, parent: e2, child: e1) + er2 = create(:enterprise_relationship, parent: e3, child: e2) + er3 = create(:enterprise_relationship, parent: e1, child: e3) + + EnterpriseRelationship.permitting([e1, e2]).sort.should == [er1, er2] + end + + it "finds relationships that grant a particular permission" do + er1 = create(:enterprise_relationship, parent: e1, child: e2, + permissions_list: ['one', 'two']) + er2 = create(:enterprise_relationship, parent: e2, child: e3, + permissions_list: ['two', 'three']) + er3 = create(:enterprise_relationship, parent: e3, child: e1, + permissions_list: ['three', 'four']) + + EnterpriseRelationship.with_permission('two').sort.should == [er1, er2].sort + end end end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index e4f979dc5b..9785d5ed2d 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -368,107 +368,6 @@ describe OrderCycle do end end - describe "calculating fees for a variant via a particular distributor" do - it "sums all the per-item fees for the variant in the specified hub + order cycle" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20) - enterprise_fee2 = create(:enterprise_fee, amount: 3) - enterprise_fee3 = create(:enterprise_fee, - calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) - product = create(:simple_product) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) - - order_cycle.fees_for(product.master, distributor).should == 23 - end - - - it "sums percentage fees for the variant" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) - product = create(:simple_product, price: 10.00) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1], variants: [product.master]) - - product.master.price.should == 10.00 - order_cycle.fees_for(product.master, distributor).should == 2.00 - end - end - - describe "creating adjustments for a line item" do - let(:oc) { OrderCycle.new } - let(:variant) { double(:variant) } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - let(:line_item) { double(:line_item, variant: variant, order: order) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_line_item_adjustment).with(line_item) - oc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant, distributor) { [applicator] } - - oc.send(:create_line_item_adjustments_for, line_item) - end - - it "makes fee applicators for a line item" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } - - oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } - - oc.send(:per_item_enterprise_fee_applicators_for, line_item.variant, distributor).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] - end - end - - describe "creating adjustments for an order" do - let(:oc) { OrderCycle.new } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_order_adjustment).with(order) - oc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } - - oc.send(:create_order_adjustments_for, order) - end - - it "makes fee applicators for an order" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } - - oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } - - oc.send(:per_order_enterprise_fee_applicators_for, order).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] - end - end - describe "finding recently closed order cycles" do it "should give the most recently closed order cycle for a distributor" do distributor = create(:distributor_enterprise) diff --git a/spec/models/product_distribution_spec.rb b/spec/models/product_distribution_spec.rb index 44ebb3cfd3..7e1a6933bc 100644 --- a/spec/models/product_distribution_spec.rb +++ b/spec/models/product_distribution_spec.rb @@ -25,6 +25,8 @@ describe ProductDistribution do describe "adjusting orders" do context "integration" do it "creates an adjustment for product distributions" do + pending "Intermittently failing spec - we intend to remove product distributions soon" + # Given an order distributor = create(:distributor_enterprise) order = create(:order, distributor: distributor) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 9705fee70e..cf17d7fe3a 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -6,19 +6,62 @@ module Spree describe User do + describe "broad permissions" do + subject { AbilityDecorator.new(user) } + let(:user) { create(:user) } + let(:enterprise_full) { create(:enterprise, type: 'full') } + let(:enterprise_single) { create(:enterprise, type: 'single') } + let(:enterprise_profile) { create(:enterprise, type: 'profile') } + + describe "managing enterprises" do + it "can manage enterprises when the user has at least one enterprise assigned" do + user.enterprise_roles.create! enterprise: enterprise_full + subject.can_manage_enterprises?(user).should be_true + end + + it "can't otherwise" do + subject.can_manage_enterprises?(user).should be_false + end + end + + describe "managing products" do + it "can when a user manages a 'full' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_full + subject.can_manage_products?(user).should be_true + end + + it "can when a user manages a 'single' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_single + subject.can_manage_products?(user).should be_true + end + + it "can't when a user manages a 'profile' type enterprise" do + user.enterprise_roles.create! enterprise: enterprise_profile + subject.can_manage_products?(user).should be_false + end + + it "can't when the user manages no enterprises" do + subject.can_manage_products?(user).should be_false + end + end + end + describe 'Roles' do # create enterprises let(:s1) { create(:supplier_enterprise) } let(:s2) { create(:supplier_enterprise) } + let(:s_related) { create(:supplier_enterprise) } let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } let(:p1) { create(:product, supplier: s1, distributors:[d1, d2]) } let(:p2) { create(:product, supplier: s2, distributors:[d1, d2]) } + let(:p_related) { create(:product, supplier: s_related) } let(:er1) { create(:enterprise_relationship, parent: s1, child: d1) } let(:er2) { create(:enterprise_relationship, parent: d1, child: s1) } + let(:er_p) { create(:enterprise_relationship, parent: s_related, child: s1, permissions_list: [:manage_products]) } subject { user } let(:user) { nil } @@ -34,12 +77,20 @@ module Spree let(:order) {create(:order)} - it "should be able to read/write their enterprises' products" do + it "should be able to read/write their enterprises' products and variants" do should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p1) + should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p1.master) end - it "should not be able to read/write other enterprises' products" do + it "should be able to read/write related enterprises' products and variants with manage_products permission" do + er_p + should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) + should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p_related.master) + end + + it "should not be able to read/write other enterprises' products and variants" do should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) + should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end it "should not be able to access admin actions on orders" do @@ -207,7 +258,7 @@ module Spree end end - context 'Enterprise manager' do + context 'enterprise manager' do let (:user) do user = create(:user) user.spree_roles = [] @@ -224,7 +275,7 @@ module Spree end it 'should have the ability administrate and create enterpises' do - should have_ability([:admin, :index, :create], for: Enterprise) + should have_ability([:admin, :index, :for_order_cycle, :create], for: Enterprise) end end end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 898a08d075..63c315a18a 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -22,9 +22,11 @@ describe Spree::Order do end describe "Payment methods" do - let(:order) { build(:order, distributor: create(:distributor_enterprise)) } - let(:pm1) { create(:payment_method, distributors: [order.distributor])} - let(:pm2) { create(:payment_method, distributors: [])} + let(:order_distributor) { create(:distributor_enterprise) } + let(:some_other_distributor) { create(:distributor_enterprise) } + let(:order) { build(:order, distributor: order_distributor) } + let(:pm1) { create(:payment_method, distributors: [order_distributor])} + let(:pm2) { create(:payment_method, distributors: [some_other_distributor])} it "finds the correct payment methods" do Spree::PaymentMethod.stub(:available).and_return [pm1, pm2] @@ -82,8 +84,10 @@ describe Spree::Order do subject.stub(:provided_by_order_cycle?) { true } order_cycle = double(:order_cycle) - order_cycle.should_receive(:create_line_item_adjustments_for).with(line_item) - order_cycle.stub(:create_order_adjustments_for) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. + should_receive(:create_line_item_adjustments_for). + with(line_item) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.stub(:create_order_adjustments_for) subject.stub(:order_cycle) { order_cycle } subject.update_distribution_charge! @@ -94,7 +98,10 @@ describe Spree::Order do subject.stub(:line_items) { [] } order_cycle = double(:order_cycle) - order_cycle.should_receive(:create_order_adjustments_for).with(subject) + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. + should_receive(:create_order_adjustments_for). + with(subject) + subject.stub(:order_cycle) { order_cycle } subject.update_distribution_charge! diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index ef70afd426..692db7c7a4 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -9,5 +9,20 @@ module Spree PaymentMethod.by_name.should == [pm2, pm3, pm1] end + + it "raises errors when required fields are missing" do + pm = PaymentMethod.new() + pm.save + pm.errors.to_a.should == ["Name can't be blank", "At least one hub must be selected"] + end + + it "generates a clean name for known Payment Method types" do + Spree::PaymentMethod::Check.clean_name.should == "Cash/EFT/etc. (payments for which automatic validation is not required)" + Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" + Spree::Gateway::PayPalExpress.clean_name.should == "PayPal Express" + + # Testing else condition + Spree::Gateway::BogusSimple.clean_name.should == "BogusSimple" + end end end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index bba8a95301..f82d07674f 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -27,12 +27,20 @@ module Spree product.should_not be_valid end - it "should default available_on to now" do + it "defaults available_on to now" do Timecop.freeze product = Product.new product.available_on.should == Time.now end + it "does not save when master is invalid" do + s = create(:supplier_enterprise) + t = create(:taxon) + product = Product.new supplier_id: s.id, name: "Apples", price: 1, primary_taxon_id: t.id + product.on_hand = "10,000" + product.save.should be_false + end + context "when the product has variants" do let(:product) do product = create(:simple_product) diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 5ddff963df..7fb160ce3f 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -82,17 +82,32 @@ module Spree describe "calculating the fees" do - it "delegates to order cycle" do + it "delegates to EnterpriseFeeCalculator" do distributor = double(:distributor) order_cycle = double(:order_cycle) variant = Variant.new - order_cycle.should_receive(:fees_for).with(variant, distributor) { 23 } + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_for).with(variant) { 23 } + variant.fees_for(distributor, order_cycle).should == 23 end end + describe "calculating fees broken down by fee type" do + it "delegates to EnterpriseFeeCalculator" do + distributor = double(:distributor) + order_cycle = double(:order_cycle) + variant = Variant.new + fees = double(:fees) + + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_by_type_for).with(variant) { fees } + + variant.fees_by_type_for(distributor, order_cycle).should == fees + end + end + + context "when the product has variants" do let!(:product) { create(:simple_product) } let!(:variant) { create(:variant, product: product) } diff --git a/spec/serializers/spree/product_serializer_spec.rb b/spec/serializers/spree/product_serializer_spec.rb new file mode 100644 index 0000000000..bbb65c1c7e --- /dev/null +++ b/spec/serializers/spree/product_serializer_spec.rb @@ -0,0 +1,7 @@ +describe Spree::Api::ProductSerializer do + let(:product) { create(:simple_product) } + it "serializes a product" do + serializer = Spree::Api::ProductSerializer.new product + serializer.to_json.should match product.name + end +end \ No newline at end of file diff --git a/spec/serializers/spree/variant_serializer_spec.rb b/spec/serializers/spree/variant_serializer_spec.rb new file mode 100644 index 0000000000..e0f26d3423 --- /dev/null +++ b/spec/serializers/spree/variant_serializer_spec.rb @@ -0,0 +1,7 @@ +describe Spree::Api::VariantSerializer do + let(:variant) { create(:variant) } + it "serializes a variant" do + serializer = Spree::Api::VariantSerializer.new variant + serializer.to_json.should match variant.options_text + end +end \ No newline at end of file diff --git a/spec/support/request/admin_helper.rb b/spec/support/request/admin_helper.rb new file mode 100644 index 0000000000..ff0c50b6f5 --- /dev/null +++ b/spec/support/request/admin_helper.rb @@ -0,0 +1,5 @@ +module AdminHelper + def have_admin_menu_item(menu_item_name) + have_selector "ul[data-hook='admin_tabs'] li", text: menu_item_name + end +end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index e8fd6e3b17..6c47d6f706 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -7,13 +7,21 @@ module ShopWorkflow have_selector ".price", text: price end + def add_enterprise_fee(enterprise_fee) + order_cycle.exchanges.outgoing.first.enterprise_fees << enterprise_fee + end + def set_order(order) ApplicationController.any_instance.stub(:session).and_return({order_id: order.id, access_token: order.token}) end def add_product_to_cart create(:line_item, variant: product.master, order: order) - order.reload.save! # Recalculate totals + order.reload + + # Recalculate totals + order.save! + order.update_distribution_charge! end def toggle_accordion(name) diff --git a/spec/support/request/ui_component_helper.rb b/spec/support/request/ui_component_helper.rb index 7ca025e235..d9f01b447b 100644 --- a/spec/support/request/ui_component_helper.rb +++ b/spec/support/request/ui_component_helper.rb @@ -68,6 +68,10 @@ module UIComponentHelper find("#cart").click end + def cart_dirty + page.find("span.cart-span")[:class].include? 'dirty' + end + def wait_for_ajax counter = 0 while page.execute_script("return $.active").to_i > 0 diff --git a/spec/views/admin/enterprises/index.rabl_spec.rb b/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb similarity index 79% rename from spec/views/admin/enterprises/index.rabl_spec.rb rename to spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb index 91463c2212..1585d08e43 100644 --- a/spec/views/admin/enterprises/index.rabl_spec.rb +++ b/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe "admin/enterprises/index.rabl" do +describe "admin/enterprises/for_order_cycle.rabl" do let(:enterprise) { create(:distributor_enterprise) } let!(:product) { create(:simple_product, supplier: enterprise) } let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 1.day.ago) } - let(:render) { Rabl.render([enterprise], 'admin/enterprises/index', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } + let(:render) { Rabl.render([enterprise], 'admin/enterprises/for_order_cycle', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } describe "supplied products" do it "does not render deleted products" do diff --git a/spec/views/admin/json/enterprise_relationships_rabl_spec.rb b/spec/views/admin/json/enterprise_relationships_rabl_spec.rb deleted file mode 100644 index 9b72a0d45b..0000000000 --- a/spec/views/admin/json/enterprise_relationships_rabl_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe "admin/json/_enterprise_relationships.json.rabl" do - let(:parent) { create(:enterprise) } - let(:child) { create(:enterprise) } - let(:enterprise_relationship) { create(:enterprise_relationship, parent: parent, child: child) } - let(:render) { Rabl.render([enterprise_relationship], 'admin/json/enterprise_relationships', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } - - it "renders a list of enterprise relationships" do - render.should have_json_type(Array).at_path '' - render.should have_json_type(Object).at_path '0' - end - - it "renders enterprise ids" do - render.should be_json_eql(parent.id).at_path '0/parent_id' - render.should be_json_eql(child.id).at_path '0/child_id' - end - - it "renders enterprise names" do - render.should be_json_eql(parent.name.to_json).at_path '0/parent_name' - render.should be_json_eql(child.name.to_json).at_path '0/child_name' - end -end diff --git a/vendor/assets/javascripts/angular-timer.min.js b/vendor/assets/javascripts/angular-timer.min.js deleted file mode 100755 index 9fdc966a93..0000000000 --- a/vendor/assets/javascripts/angular-timer.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * angular-timer - v1.1.6 - 2014-07-01 7:37 AM - * https://github.com/siddii/angular-timer - * - * Copyright (c) 2014 Siddique Hameed - * Licensed MIT - */ -var timerModule=angular.module("timer",[]).directive("timer",["$compile",function(a){return{restrict:"EAC",replace:!1,scope:{interval:"=interval",startTimeAttr:"=startTime",endTimeAttr:"=endTime",countdownattr:"=countdown",finishCallback:"&finishCallback",autoStart:"&autoStart",maxTimeUnit:"="},controller:["$scope","$element","$attrs","$timeout",function(b,c,d,e){function f(){b.timeoutId&&clearTimeout(b.timeoutId)}function g(){b.maxTimeUnit&&"day"!==b.maxTimeUnit?"second"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3),b.minutes=0,b.hours=0,b.days=0,b.months=0,b.years=0):"minute"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4),b.hours=0,b.days=0,b.months=0,b.years=0):"hour"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5),b.days=0,b.months=0,b.years=0):"month"===b.maxTimeUnit?(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24%30),b.months=Math.floor(b.millis/36e5/24/30),b.years=0):"year"===b.maxTimeUnit&&(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24%30),b.months=Math.floor(b.millis/36e5/24/30%12),b.years=Math.floor(b.millis/36e5/24/365)):(b.seconds=Math.floor(b.millis/1e3%60),b.minutes=Math.floor(b.millis/6e4%60),b.hours=Math.floor(b.millis/36e5%24),b.days=Math.floor(b.millis/36e5/24),b.months=0,b.years=0),b.secondsS=1==b.seconds?"":"s",b.minutesS=1==b.minutes?"":"s",b.hoursS=1==b.hours?"":"s",b.daysS=1==b.days?"":"s",b.monthsS=1==b.months?"":"s",b.yearsS=1==b.years?"":"s",b.sseconds=b.seconds<10?"0"+b.seconds:b.seconds,b.mminutes=b.minutes<10?"0"+b.minutes:b.minutes,b.hhours=b.hours<10?"0"+b.hours:b.hours,b.ddays=b.days<10?"0"+b.days:b.days,b.mmonths=b.months<10?"0"+b.months:b.months,b.yyears=b.years<10?"0"+b.years:b.years}"function"!=typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),b.autoStart=d.autoStart||d.autostart,c.append(0===c.html().trim().length?a("{{millis}}")(b):a(c.contents())(b)),b.startTime=null,b.endTime=null,b.timeoutId=null,b.countdown=b.countdownattr&&parseInt(b.countdownattr,10)>=0?parseInt(b.countdownattr,10):void 0,b.isRunning=!1,b.$on("timer-start",function(){b.start()}),b.$on("timer-resume",function(){b.resume()}),b.$on("timer-stop",function(){b.stop()}),b.$on("timer-clear",function(){b.clear()}),b.$on("timer-set-countdown",function(a,c){b.countdown=c}),b.start=c[0].start=function(){b.startTime=b.startTimeAttr?new Date(b.startTimeAttr):new Date,b.endTime=b.endTimeAttr?new Date(b.endTimeAttr):null,b.countdown||(b.countdown=b.countdownattr&&parseInt(b.countdownattr,10)>0?parseInt(b.countdownattr,10):void 0),f(),h(),b.isRunning=!0},b.resume=c[0].resume=function(){f(),b.countdownattr&&(b.countdown+=1),b.startTime=new Date-(b.stoppedTime-b.startTime),h(),b.isRunning=!0},b.stop=b.pause=c[0].stop=c[0].pause=function(){var a=b.timeoutId;b.clear(),b.$emit("timer-stopped",{timeoutId:a,millis:b.millis,seconds:b.seconds,minutes:b.minutes,hours:b.hours,days:b.days})},b.clear=c[0].clear=function(){b.stoppedTime=new Date,f(),b.timeoutId=null,b.isRunning=!1},c.bind("$destroy",function(){f(),b.isRunning=!1}),b.countdownattr?(b.millis=1e3*b.countdownattr,b.addCDSeconds=c[0].addCDSeconds=function(a){b.countdown+=a,b.$digest(),b.isRunning||b.start()},b.$on("timer-add-cd-seconds",function(a,c){e(function(){b.addCDSeconds(c)})}),b.$on("timer-set-countdown-seconds",function(a,c){b.isRunning||b.clear(),b.countdown=c,b.millis=1e3*c,g()})):b.millis=0,g();var h=function(){b.millis=new Date-b.startTime;var a=b.millis%1e3;return b.endTimeAttr&&(b.millis=b.endTime-new Date,a=b.interval-b.millis%1e3),b.countdownattr&&(b.millis=1e3*b.countdown),b.millis<0?(b.stop(),b.millis=0,g(),void(b.finishCallback&&b.$eval(b.finishCallback))):(g(),b.timeoutId=setTimeout(function(){h(),b.$digest()},b.interval-a),b.$emit("timer-tick",{timeoutId:b.timeoutId,millis:b.millis}),void(b.countdown>0?b.countdown--:b.countdown<=0&&(b.stop(),b.finishCallback&&b.$eval(b.finishCallback))))};(void 0===b.autoStart||b.autoStart===!0)&&b.start()}]}}]);"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports=timerModule); \ No newline at end of file