diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 0000000000..867e417e06 --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,4 @@ +--format progress +--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log +--tag ~performance diff --git a/Gemfile b/Gemfile index ac1697e3f9..d8e4c08bc9 100644 --- a/Gemfile +++ b/Gemfile @@ -51,6 +51,8 @@ gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.0' gem 'roadie-rails', '~> 1.0.3' gem 'figaro' +gem 'acts-as-taggable-on', '~> 3.4' + gem 'foreigner' gem 'immigrant' @@ -113,4 +115,5 @@ group :development do gem 'guard-rails' gem 'guard-zeus' gem 'guard-rspec' + gem 'parallel_tests' end diff --git a/Gemfile.lock b/Gemfile.lock index f919568a7b..244346a15f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,8 @@ GEM activesupport (3.2.21) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) @@ -249,8 +251,7 @@ GEM erubis (2.7.0) eventmachine (1.0.3) excon (0.25.3) - execjs (1.4.0) - multi_json (~> 1.0) + execjs (2.5.2) factory_girl (3.3.0) activesupport (>= 3.0.0) factory_girl_rails (3.3.0) @@ -326,7 +327,7 @@ GEM kaminari (0.14.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.7.4) + kgio (2.9.3) launchy (2.1.2) addressable (~> 2.3) letter_opener (1.0.0) @@ -362,6 +363,9 @@ GEM activesupport (>= 3.0.0) cocaine (~> 0.5.3) mime-types + parallel (1.4.1) + parallel_tests (1.3.7) + parallel paypal-sdk-core (0.2.10) multi_json (~> 1.0) xml-simple @@ -413,7 +417,7 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.9.0) + raindrops (0.13.0) rake (10.4.2) ransack (0.7.2) actionpack (~> 3.0) @@ -497,10 +501,10 @@ GEM turn (0.8.3) ansi tzinfo (0.3.44) - uglifier (1.2.4) + uglifier (2.7.1) execjs (>= 0.3.0) - multi_json (>= 1.0.2) - unicorn (4.3.1) + json (>= 1.8.0) + unicorn (4.9.0) kgio (~> 2.6) rack raindrops (~> 0.7) @@ -532,6 +536,7 @@ PLATFORMS DEPENDENCIES active_model_serializers + acts-as-taggable-on (~> 3.4) andand angular-rails-templates angularjs-file-upload-rails (~> 1.1.0) @@ -576,6 +581,7 @@ DEPENDENCIES nokogiri oj paperclip + parallel_tests pg poltergeist pry-debugger diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee index 1c9f65f91a..ffe2c9eaaa 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", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 9a07309352..e44e6ab96d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -17,9 +17,13 @@ //= require admin/spree_promo //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js +//= require ../shared/ng-tags-input.min.js //= require ./admin +//= require ./customers/customers +//= require ./dropdown/dropdown //= require ./enterprises/enterprises //= require ./enterprise_groups/enterprise_groups +//= require ./index_utils/index_utils //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index eb99f360af..5c2b5b8984 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -1,6 +1,6 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ - "$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey" - ($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey) -> + "$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns" + ($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) -> $scope.loading = true $scope.initialiseVariables = -> @@ -18,9 +18,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.selectedUnitsProduct = {}; $scope.selectedUnitsVariant = {}; $scope.sharedResource = false - $scope.predicate = "" - $scope.reverse = false - $scope.columns = + $scope.columns = Columns.setColumns order_no: { name: "Order No.", visible: false } full_name: { name: "Name", visible: true } email: { name: "Email", visible: false } diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 177859235b..b3d24f00c0 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -1,9 +1,9 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, tax_categories) -> +angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories) -> $scope.loading = true $scope.StatusMessage = StatusMessage - $scope.columns = + $scope.columns = Columns.setColumns producer: {name: "Producer", visible: true} sku: {name: "SKU", visible: false} name: {name: "Name", visible: true} @@ -109,6 +109,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit" + $scope.toggleShowAllVariants = -> + showVariants = !DisplayProperties.showVariants 0 + $scope.filteredProducts.forEach (product) -> + DisplayProperties.setShowVariants product.id, showVariants + DisplayProperties.setShowVariants 0, showVariants + $scope.addVariant = (product) -> product.variants.push id: $scope.nextVariantId() diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee new file mode 100644 index 0000000000..c475f1e4df --- /dev/null +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -0,0 +1,17 @@ +angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) -> + $scope.shop = null + $scope.shops = shops + $scope.submitAll = pendingChanges.submitAll + + $scope.columns = Columns.setColumns + email: { name: "Email", visible: true } + code: { name: "Code", visible: true } + tags: { name: "Tags", visible: true } + + $scope.$watch "shop", -> + if $scope.shop? + Customers.loaded = false + $scope.customers = Customers.index(enterprise_id: $scope.shop.id) + + $scope.loaded = -> + Customers.loaded diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee new file mode 100644 index 0000000000..3733fe2eea --- /dev/null +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -0,0 +1 @@ +angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee new file mode 100644 index 0000000000..e15ec10342 --- /dev/null +++ b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.customers").directive "tagsWithTranslation", -> + restrict: "E" + template: "" + scope: + object: "=" + link: (scope, element, attrs) -> + scope.$watchCollection "object.tags", -> + scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",") diff --git a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee new file mode 100644 index 0000000000..523e0c1495 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.customers").factory 'CustomerResource', ($resource) -> + $resource('/admin/customers.json', {}, { + 'index': + method: 'GET' + isArray: true + params: + enterprise_id: '@enterprise_id' + }) diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee new file mode 100644 index 0000000000..9acfa317d2 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -0,0 +1,16 @@ +angular.module("admin.customers").factory 'Customers', (CustomerResource) -> + new class Customers + customers: [] + customers_by_id: {} + loaded: false + + index: (params={}, callback=null) -> + CustomerResource.index params, (data) => + for customer in data + @customers.push customer + @customers_by_id[customer.id] = customer + + @loaded = true + (callback || angular.noop)(@customers) + + @customers diff --git a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee deleted file mode 100644 index c5afce07a5..0000000000 --- a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -angular.module("ofn.admin").directive "ofnLineItemUpdAttr", [ - "switchClass", "pendingChanges" - (switchClass, pendingChanges) -> - require: "ngModel" - link: (scope, element, attrs, ngModel) -> - attrName = attrs.ofnLineItemUpdAttr - element.dbValue = scope.$eval(attrs.ngModel) - scope.$watch -> - scope.$eval(attrs.ngModel) - , (value) -> - #if ngModel.$dirty - # i think i can take this out, this directive is still only called - # on a change and only an updated value will create a db call. - if value == element.dbValue - pendingChanges.remove(scope.line_item.id, attrName) - switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) - else - changeObj = - lineItem: scope.line_item - element: element - attrName: attrName - url: "/api/orders/#{scope.line_item.order.number}/line_items/#{scope.line_item.id}?line_item[#{attrName}]=#{value}" - pendingChanges.add(scope.line_item.id, attrName, changeObj) - switchClass( element, "update-pending", ["update-error", "update-success"], false ) -] diff --git a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee index 410df8d7e9..f5b18ae5cb 100644 --- a/app/assets/javascripts/admin/directives/toggle_variants.js.coffee +++ b/app/assets/javascripts/admin/directives/toggle_variants.js.coffee @@ -1,10 +1,8 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -> link: (scope, element, attrs) -> if DisplayProperties.showVariants scope.product.id - element.removeClass "icon-chevron-right" element.addClass "icon-chevron-down" else - element.removeClass "icon-chevron-down" element.addClass "icon-chevron-right" element.on "click", -> @@ -16,4 +14,4 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) - else DisplayProperties.setShowVariants scope.product.id, true element.removeClass "icon-chevron-right" - element.addClass "icon-chevron-down" \ No newline at end of file + element.addClass "icon-chevron-down" diff --git a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee new file mode 100644 index 0000000000..02e47ff9f7 --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) -> + $scope.expanded = false diff --git a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee new file mode 100644 index 0000000000..9b506cb8fb --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee @@ -0,0 +1,5 @@ + angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) -> + link: (scope, element, attrs) -> + element.click (event) -> + event.stopPropagation() + scope.$emit "offClick" diff --git a/app/assets/javascripts/admin/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee similarity index 67% rename from app/assets/javascripts/admin/dropdown.js.coffee rename to app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee index e18407abcc..560598d23e 100644 --- a/app/assets/javascripts/admin/dropdown.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee @@ -1,6 +1,4 @@ -dropDownModule = angular.module("ofn.dropdown", []) - -dropDownModule.directive "ofnDropDown", ($document) -> + angular.module("admin.dropdown").directive "ofnDropDown", ($document) -> link: (scope, element, attrs) -> outsideClickListener = (event) -> unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") || @@ -20,12 +18,3 @@ dropDownModule.directive "ofnDropDown", ($document) -> scope.$apply -> scope.expanded = true element.addClass "expanded" - -dropDownModule.directive "ofnCloseOnClick", ($document) -> - link: (scope, element, attrs) -> - element.click (event) -> - event.stopPropagation() - scope.$emit "offClick" - -dropDownModule.controller "DropDownCtrl", ($scope) -> - $scope.expanded = false \ No newline at end of file diff --git a/app/assets/javascripts/admin/dropdown/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/dropdown.js.coffee new file mode 100644 index 0000000000..ae6118390f --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/dropdown.js.coffee @@ -0,0 +1 @@ +angular.module("admin.dropdown", []) diff --git a/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee b/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee new file mode 100644 index 0000000000..39556983b3 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/controllers/columns_controller.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.indexUtils").controller "ColumnsCtrl", ($scope, Columns) -> + $scope.columns = Columns.columns + $scope.predicate = "" + $scope.reverse = false diff --git a/app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee b/app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee new file mode 100644 index 0000000000..254fa5c438 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/obj_for_update.js.coffee @@ -0,0 +1,36 @@ +angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendingChanges) -> + scope: + object: "&objForUpdate" + type: "@objForUpdate" + attr: "@attrForUpdate" + link: (scope, element, attrs) -> + scope.savedValue = scope.object()[scope.attr] + + scope.$watch "object().#{scope.attr}", (value) -> + if value == scope.savedValue + pendingChanges.remove(scope.object().id, scope.attr) + scope.clear() + else + change = + object: scope.object() + type: scope.type + attr: scope.attr + value: value + scope: scope + scope.pending() + pendingChanges.add(scope.object().id, scope.attr, change) + + scope.reset = (value) -> + scope.savedValue = value + + scope.success = -> + switchClass( element, "update-success", ["update-pending", "update-error"], 3000 ) + + scope.pending = -> + switchClass( element, "update-pending", ["update-error", "update-success"], false ) + + scope.error = -> + switchClass( element, "update-error", ["update-pending", "update-success"], false ) + + scope.clear = -> + switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) diff --git a/app/assets/javascripts/admin/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee similarity index 74% rename from app/assets/javascripts/admin/directives/toggle_column.js.coffee rename to app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index 1b8487eeb1..d6239ff47f 100644 --- a/app/assets/javascripts/admin/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").directive "ofnToggleColumn", -> +angular.module("admin.indexUtils").directive "ofnToggleColumn", -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible element.click "click", -> @@ -8,4 +8,4 @@ angular.module("ofn.admin").directive "ofnToggleColumn", -> element.removeClass "selected" else scope.column.visible = true - element.addClass "selected" \ No newline at end of file + element.addClass "selected" diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee new file mode 100644 index 0000000000..0925bf45ed --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -0,0 +1 @@ +angular.module("admin.indexUtils", ['ngResource']).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/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee new file mode 100644 index 0000000000..46e7ccd3b8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.indexUtils").factory 'Columns', -> + new class Columns + columns: {} + + setColumns: (columns) -> + @columns = {} + @columns[name] = column for name, column of columns + @columns diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee new file mode 100644 index 0000000000..2f40a7faef --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -0,0 +1,33 @@ +angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> + new class pendingChanges + pendingChanges: {} + + add: (id, attr, change) => + @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") + @pendingChanges["#{id}"]["#{attr}"] = change + + removeAll: => + @pendingChanges = {} + + remove: (id, attr) => + if @pendingChanges.hasOwnProperty("#{id}") + delete @pendingChanges["#{id}"]["#{attr}"] + delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 + + submitAll: => + all = [] + for id, objectChanges of @pendingChanges + for attrName, change of objectChanges + all.push @submit(change) + all + + submit: (change) -> + resources.update(change).$promise.then (data) => + @remove change.object.id, change.attr + change.scope.reset( data["#{change.attr}"] ) + change.scope.success() + , (error) -> + change.scope.error() + + changeCount: (objectChanges) -> + Object.keys(objectChanges).length diff --git a/app/assets/javascripts/admin/index_utils/services/resources.js.coffee b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee new file mode 100644 index 0000000000..65dad204d4 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/resources.js.coffee @@ -0,0 +1,30 @@ +angular.module("admin.indexUtils").factory "resources", ($resource) -> + LineItem = $resource '/api/orders/:order_number/line_items/:line_item_id.json', + { order_number: '@order_number', line_item_id: '@line_item_id'}, + 'update': { method: 'PUT' } + Customer = $resource '/admin/customers/:customer_id.json', + { customer_id: '@customer_id'}, + 'update': { method: 'PUT' } + + return { + update: (change) -> + params = {} + data = {} + resource = null + + switch change.type + when "line_item" + resource = LineItem + params.order_number = change.object.order.number + params.line_item_id = change.object.id + data.line_item = {} + data.line_item[change.attr] = change.value + when "customer" + resource = Customer + params.customer_id = change.object.id + data.customer = {} + data.customer[change.attr] = change.value + else "" + + resource.update(params, data) + } diff --git a/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee new file mode 100644 index 0000000000..c2a3419e2c --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee @@ -0,0 +1,10 @@ +angular.module("admin.indexUtils").factory "switchClass", ($timeout) -> + return (element,classToAdd,removeClasses,timeout) -> + $timeout.cancel element.timeout if element.timeout + element.removeClass className for className in removeClasses + element.addClass classToAdd + intRegex = /^\d+$/ + if timeout && intRegex.test(timeout) + element.timeout = $timeout(-> + element.removeClass classToAdd + , timeout, true) diff --git a/app/assets/javascripts/admin/services/data_submitter.js.coffee b/app/assets/javascripts/admin/services/data_submitter.js.coffee deleted file mode 100644 index 7d121ec645..0000000000 --- a/app/assets/javascripts/admin/services/data_submitter.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -angular.module("ofn.admin").factory "dataSubmitter", [ - "$http", "$q", "switchClass" - ($http, $q, switchClass) -> - return (changeObj) -> - deferred = $q.defer() - $http.put(changeObj.url).success((data) -> - switchClass changeObj.element, "update-success", ["update-pending", "update-error"], 3000 - deferred.resolve data - ).error -> - switchClass changeObj.element, "update-error", ["update-pending", "update-success"], false - deferred.reject() - deferred.promise -] \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/display_properties.js.coffee b/app/assets/javascripts/admin/services/display_properties.js.coffee index 7288706032..3037c9f068 100644 --- a/app/assets/javascripts/admin/services/display_properties.js.coffee +++ b/app/assets/javascripts/admin/services/display_properties.js.coffee @@ -3,12 +3,10 @@ angular.module("ofn.admin").factory "DisplayProperties", -> displayProperties: {} showVariants: (product_id) -> - @initProduct product_id - @displayProperties[product_id].showVariants + @productProperties(product_id).showVariants setShowVariants: (product_id, showVariants) -> - @initProduct product_id - @displayProperties[product_id].showVariants = showVariants + @productProperties(product_id).showVariants = showVariants - initProduct: (product_id) -> + productProperties: (product_id) -> @displayProperties[product_id] ||= {showVariants: false} diff --git a/app/assets/javascripts/admin/services/pending_changes.js.coffee b/app/assets/javascripts/admin/services/pending_changes.js.coffee deleted file mode 100644 index d72a4ac7bc..0000000000 --- a/app/assets/javascripts/admin/services/pending_changes.js.coffee +++ /dev/null @@ -1,32 +0,0 @@ -angular.module("ofn.admin").factory "pendingChanges",[ - "dataSubmitter" - (dataSubmitter) -> - pendingChanges: {} - - add: (id, attrName, changeObj) -> - @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") - @pendingChanges["#{id}"]["#{attrName}"] = changeObj - - removeAll: -> - @pendingChanges = {} - - remove: (id, attrName) -> - if @pendingChanges.hasOwnProperty("#{id}") - delete @pendingChanges["#{id}"]["#{attrName}"] - delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - - submitAll: -> - all = [] - for id,lineItem of @pendingChanges - for attrName,changeObj of lineItem - all.push @submit(id, attrName, changeObj) - all - - submit: (id, attrName, change) -> - dataSubmitter(change).then (data) => - @remove id, attrName - change.element.dbValue = data["#{attrName}"] - - changeCount: (lineItem) -> - Object.keys(lineItem).length -] \ No newline at end of file diff --git a/app/assets/javascripts/admin/services/switch_class.js.coffee b/app/assets/javascripts/admin/services/switch_class.js.coffee deleted file mode 100644 index e39c52d1f6..0000000000 --- a/app/assets/javascripts/admin/services/switch_class.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -angular.module("ofn.admin").factory "switchClass", [ - "$timeout" - ($timeout) -> - return (element,classToAdd,removeClasses,timeout) -> - $timeout.cancel element.timeout if element.timeout - element.removeClass className for className in removeClasses - element.addClass classToAdd - intRegex = /^\d+$/ - if timeout && intRegex.test(timeout) - element.timeout = $timeout(-> - element.removeClass classToAdd - , timeout, true) -] \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index a07ae1f466..4785adae85 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -32,8 +32,9 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro if product.variants product.variants = (Variants.register variant for variant in product.variants) variant.product = product for variant in product.variants - product.master.product = product - product.master = Variants.register product.master if product.master + if product.master + product.master.product = product + product.master = Variants.register product.master registerVariantsWithCart: -> for product in @products diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js new file mode 100755 index 0000000000..9a1acd6e0d --- /dev/null +++ b/app/assets/javascripts/shared/ng-tags-input.min.js @@ -0,0 +1 @@ +/*! ngTagsInput v2.3.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tags},getCurrentTagText:function(){return a.newTag.text},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",b.tags.length<=n.maxTags),i.$setValidity("minTags",b.tags.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text)},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:"",invalid:null,setText:function(a){this.text=a,m.trigger("input-change",a)}},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){b.tags=h.makeObjectArray(a,n.displayProperty),l.items=b.tags}),b.$watch("tags.length",function(){j()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{change:function(a){m.trigger("input-change",a)},keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-added",function(){b.newTag.setText("")}).on("tag-added tag-removed",function(){i.$setViewValue(b.tags)}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,h=c.keyCode,i=c.shiftKey||c.altKey||c.ctrlKey||c.metaKey,j={};if(!i&&-1!==k.indexOf(h)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[h],e=(h===a.backspace||h===a["delete"])&&l.selected,g=h===a.backspace&&0===b.newTag.text.length&&n.enableEditingLastTag,f=(h===a.backspace||h===a.left||h===a.right)&&0===b.newTag.text.length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text);else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.setText(m[n.displayProperty])}else e?l.removeSelected():f&&(h===a.left||h===a.backspace?l.selectPrior():h===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,g=[a.enter,a.tab,a.escape,a.up,a.down],h=b.suggestionList,j=e.registerAutocomplete(),k=b.options,l=b.events;k.tagsInput=j.getOptions(),f=function(a){return a&&a.length>=k.minLength||!a&&k.loadOnEmpty},b.addSuggestionByIndex=function(a){h.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return h.selected&&(j.addTag(angular.copy(h.selected)),h.reset(),j.focusInput(),a=!0),a},b.track=function(a){return a[k.tagsInput.keyProperty||k.tagsInput.displayProperty]},j.on("tag-added invalid-tag input-blur",function(){h.reset()}).on("input-change",function(a){f(a)?h.load(a,j.getTags()):h.reset()}).on("input-focus",function(){var a=j.getCurrentTagText();k.loadOnFocus&&f(a)&&h.load(a,j.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(-1!==g.indexOf(d))return h.visible?d===a.down?(h.selectNext(),e=!0):d===a.up?(h.selectPrior(),e=!0):d===a.escape?(h.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(h.load(j.getCurrentTagText(),j.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),l.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
'),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
'),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); \ No newline at end of file diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index be9948b95d..9c57240dfe 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -14,7 +14,7 @@ .small-12.columns .alert-box.info{ "ofn-inline-alert" => true, ng: { show: "visible" } } %h6 Success! {{ enterprise.name }} added to the Open Food Network - %span If you exit the wizard at any stage, login and go to admin to edit or update your enterprise details. + %span If you exit this wizard at any stage, you need to click the confirmation link in the email you have received. This will take you to your admin interface where you can continue setting up your profile. %a.close{ ng: { click: "close()" } } × .small-12.large-8.columns diff --git a/app/assets/javascripts/templates/registration/finished.html.haml b/app/assets/javascripts/templates/registration/finished.html.haml index f647a2d8bb..46cca9daf5 100644 --- a/app/assets/javascripts/templates/registration/finished.html.haml +++ b/app/assets/javascripts/templates/registration/finished.html.haml @@ -18,7 +18,7 @@ %p We've sent a confirmation email to - %strong {{ enterprise.email }}. + %strong {{ enterprise.email }} if it hasn't been activated before. %br Please follow the instructions there to make your enterprise visible on the Open Food Network. %a.button.primary{ type: "button", href: "/" } Open Food Network home > diff --git a/app/assets/javascripts/templates/registration/introduction.html.haml b/app/assets/javascripts/templates/registration/introduction.html.haml index 60a8547b4a..48553de09a 100644 --- a/app/assets/javascripts/templates/registration/introduction.html.haml +++ b/app/assets/javascripts/templates/registration/introduction.html.haml @@ -5,7 +5,7 @@ %h4 %small %i.ofn-i_040-hub - Create your enterprise profile + You can now create a profile for your Producer or Hub .hide-for-large-up %hr %input.button.small.primary{ type: "button", value: "Let's get started!", ng: { click: "select('details')" } } @@ -38,6 +38,7 @@ %strong contact you on the Open Food Network. %p Use this space to tell the story of your enterprise, to help drive connections to your social and online presence. + %p It's also the first step towards trading on the Open Food Network, or opening an online store. .row.show-for-large-up .small-12.columns diff --git a/app/assets/javascripts/templates/registration/type.html.haml b/app/assets/javascripts/templates/registration/type.html.haml index 48d45cb66a..9593bfa89c 100644 --- a/app/assets/javascripts/templates/registration/type.html.haml +++ b/app/assets/javascripts/templates/registration/type.html.haml @@ -38,9 +38,13 @@ %i.ofn-i_013-help   %p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it. - / %p Hubs connect the producer to the eater. Hubs can be co-ops, independent retailers, buying groups, wholesalers, CSA box schemes, farm-gate stalls, etc. + .panel.callout + .left + %i.ofn-i_013-help +   + %p If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other. .row.buttons .small-12.columns %input.button.secondary{ type: "button", value: "Back", ng: { click: "select('contact')" } } - %input.button.primary.right{ type: "submit", value: "Continue" } + %input.button.primary.right{ type: "submit", value: "Create Profile" } diff --git a/app/assets/stylesheets/admin/all.css b/app/assets/stylesheets/admin/all.css index 9b3603fe7b..e0d668b95b 100644 --- a/app/assets/stylesheets/admin/all.css +++ b/app/assets/stylesheets/admin/all.css @@ -10,6 +10,7 @@ *= require shared/jquery-ui-timepicker-addon *= require shared/textAngular.min + *= require shared/ng-tags-input.min *= require_self *= require_tree . diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index c0c1dac86b..b1327630c6 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -8,16 +8,22 @@ } } -input.update-pending { - border: solid 1px orange; +input, div { + &.update-pending { + border: solid 1px orange; + } } -input.update-error { - border: solid 1px red; +input, div { + &.update-error { + border: solid 1px red; + } } -input.update-success { - border: solid 1px #9fc820; +input, div { + &.update-success { + border: solid 1px #9fc820; + } } .no-close .ui-dialog-titlebar-close { @@ -42,4 +48,4 @@ div#group_buy_calculation { .row span { text-align: center; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/shared/ng-tags-input.min.css b/app/assets/stylesheets/shared/ng-tags-input.min.css new file mode 100755 index 0000000000..ee4a4a98d5 --- /dev/null +++ b/app/assets/stylesheets/shared/ng-tags-input.min.css @@ -0,0 +1 @@ +tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0;padding-left:5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input.invalid-tag{color:red}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff} \ No newline at end of file diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb new file mode 100644 index 0000000000..27b5b1380d --- /dev/null +++ b/app/controllers/admin/customers_controller.rb @@ -0,0 +1,29 @@ +module Admin + class CustomersController < ResourceController + before_filter :load_managed_shops, only: :index, if: :html_request? + respond_to :json + + def index + respond_to do |format| + format.html + format.json do + render json: ActiveModel::ArraySerializer.new( @collection, + each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user + ).to_json + end + end + end + + private + + def collection + return Customer.where("1=0") if html_request? || params[:enterprise_id].nil? + enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) + Customer.of(enterprise) + end + + def load_managed_shops + @shops = Enterprise.managed_by(spree_current_user).is_distributor + end + end +end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index a0b55ad3c8..e50779c3ff 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index @@ -199,7 +201,8 @@ module Admin # Overriding method on Spree's resource controller def location_after_save - refered_from_edit = URI(request.referer).path == main_app.edit_admin_enterprise_path(@enterprise) + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + refered_from_edit = referer_path == main_app.edit_admin_enterprise_path(@enterprise) if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53763ad274..aaa7d0bb06 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + class ApplicationController < ActionController::Base protect_from_forgery @@ -9,7 +11,8 @@ class ApplicationController < ActionController::Base end def set_checkout_redirect - if request.referer and referer_path = URI(request.referer).path + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path session["spree_user_return_to"] = [main_app.checkout_path].include?(referer_path) ? referer_path : root_path end end diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 1d74df5706..392e2fef64 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -12,9 +12,6 @@ class BaseController < ApplicationController before_filter :check_order_cycle_expiry - def load_active_distributors - @active_distributors ||= Enterprise.distributors_with_active_order_cycles - end private diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 0c42ceb987..11be28ef04 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -12,9 +12,6 @@ class CheckoutController < Spree::CheckoutController include EnterprisesHelper def edit - # Because this controller doesn't inherit from our BaseController - # We need to duplicate the code here - @active_distributors ||= Enterprise.distributors_with_active_order_cycles end def update diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 097139556c..75ad5c475b 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -4,7 +4,7 @@ class EnterprisesController < BaseController include OrderCyclesHelper # These prepended filters are in the reverse order of execution - prepend_before_filter :load_active_distributors, :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop + prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index fe43f0a0fa..43a3f49abe 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,6 +1,5 @@ class GroupsController < BaseController layout 'darkswarm' - before_filter :load_active_distributors def index @groups = EnterpriseGroup.on_front_page.by_position diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 76e179ed22..3bb7a68538 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,11 +1,9 @@ class HomeController < BaseController layout 'darkswarm' - before_filter :load_active_distributors - + def index end def about_us end end - diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index a980ba8f40..46a6f5852a 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -1,6 +1,5 @@ class MapController < BaseController layout 'darkswarm' - before_filter :load_active_distributors def index end diff --git a/app/controllers/producers_controller.rb b/app/controllers/producers_controller.rb index b101a95b7f..42d1d401e5 100644 --- a/app/controllers/producers_controller.rb +++ b/app/controllers/producers_controller.rb @@ -1,7 +1,6 @@ class ProducersController < BaseController layout 'darkswarm' - before_filter :load_active_distributors - + def index end end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 73861def5c..0655624a0b 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -10,12 +10,16 @@ class ShopController < BaseController end def products - # Can we make this query less slow? - # if @products = products_for_shop + render status: 200, - json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, - current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json + json: ActiveModel::ArraySerializer.new(@products, + each_serializer: Api::ProductSerializer, + current_order_cycle: current_order_cycle, + current_distributor: current_distributor, + variants: variants_for_shop_by_id, + master_variants: master_variants_for_shop_by_id).to_json + else render json: "", status: 404 end @@ -56,4 +60,30 @@ class ShopController < BaseController "name ASC" end end + + def all_variants_for_shop + # We use the in_stock? method here instead of the in_stock scope because we need to + # look up the stock as overridden by VariantOverrides, and the scope method is not affected + # by them. + Spree::Variant. + for_distribution(current_order_cycle, current_distributor). + each { |v| v.scope_to_hub current_distributor }. + select(&:in_stock?) + end + + def variants_for_shop_by_id + index_by_product_id all_variants_for_shop.reject(&:is_master) + end + + def master_variants_for_shop_by_id + index_by_product_id all_variants_for_shop.select(&:is_master) + end + + def index_by_product_id(variants) + variants.inject({}) do |vs, v| + vs[v.product_id] ||= [] + vs[v.product_id] << v + vs + end + end end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 85904590c3..3fa6a5c5e1 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -58,4 +58,8 @@ Spree::Admin::BaseController.class_eval do "Until you set these up, customers will not be able to shop at this hub." end end + + def html_request? + request.format.html? + end end diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index f7d674048e..87c6f3329a 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -47,7 +47,7 @@ Spree::Admin::OrdersController.class_eval do def managed permissions = OpenFoodNetwork::Permissions.new(spree_current_user) - @orders = permissions.editable_orders.ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) + @orders = permissions.editable_orders.order(:id).ransack(params[:q]).result.page(params[:page]).per(params[:per_page]) render json: @orders, each_serializer: Api::Admin::OrderSerializer end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 1591586f76..5b1bb347a1 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -1,4 +1,5 @@ require 'open_food_network/spree_api_key_loader' +require 'open_food_network/referer_parser' Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader @@ -53,7 +54,8 @@ Spree::Admin::ProductsController.class_eval do protected def location_after_save - if URI(request.referer).path == '/admin/products/bulk_edit' + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path == '/admin/products/bulk_edit' bulk_edit_admin_products_url else location_after_save_original diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 34f2d640c2..81975bb250 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -7,6 +7,7 @@ require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' require 'open_food_network/sales_tax_report' +require 'open_food_network/xero_invoices_report' Spree::Admin::ReportsController.class_eval do @@ -679,7 +680,22 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv") end - def render_report (header, table, create_csv, csv_file_name) + def xero_invoices + if request.get? + params[:q] ||= {} + params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month + end + @distributors = Enterprise.is_distributor.managed_by(spree_current_user) + @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') + + @search = Spree::Order.complete.managed_by(spree_current_user).order('id DESC').search(params[:q]) + orders = @search.result + @report = OpenFoodNetwork::XeroInvoicesReport.new orders, params + render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") + end + + + def render_report(header, table, create_csv, csv_file_name) unless create_csv render :html => table else @@ -716,7 +732,9 @@ Spree::Admin::ReportsController.class_eval do :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, :order_cycle_management => {:name => "Order Cycle Management", :description => ''}, - :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" } + :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }, + :xero_invoices => { :name => "Xero Invoices", :description => 'Invoices for import into Xero' } + } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 961bc60afb..250a241625 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -25,6 +25,10 @@ module Admin admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end + def admin_inject_shops + admin_inject_json_ams_array "admin.customers", "shops", @shops, Api::Admin::IdNameSerializer + end + def admin_inject_hubs admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer end diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 37794cef9d..05057c136b 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -1,6 +1,16 @@ +require 'open_food_network/enterprise_injection_data' + module InjectionHelper def inject_enterprises - inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors + inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data + end + + def inject_group_enterprises + inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data + end + + def inject_current_hub + inject_json_ams "currentHub", current_distributor, Api::EnterpriseSerializer, enterprise_injection_data end def inject_current_order @@ -53,4 +63,13 @@ module InjectionHelper end render partial: "json/injection_ams", locals: {name: name, json: json} end + + + private + + def enterprise_injection_data + @enterprise_injection_data ||= OpenFoodNetwork::EnterpriseInjectionData.new + {data: @enterprise_injection_data} + end + end diff --git a/app/models/customer.rb b/app/models/customer.rb index d3fa9e093f..856f7e94d7 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,10 +1,20 @@ class Customer < ActiveRecord::Base + acts_as_taggable + belongs_to :enterprise belongs_to :user, :class_name => Spree.user_class - validates :code, presence: true, uniqueness: {scope: :enterprise_id} - validates :email, presence: true + validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true } + validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" } validates :enterprise_id, presence: true scope :of, ->(enterprise) { where(enterprise_id: enterprise) } + + before_create :associate_user + + private + + def associate_user + self.user = user || Spree::User.find_by_email(email) + end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 067e800e3c..4adfb05309 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -30,6 +30,7 @@ class Enterprise < ActiveRecord::Base has_and_belongs_to_many :payment_methods, join_table: 'distributors_payment_methods', class_name: 'Spree::PaymentMethod', foreign_key: 'distributor_id' has_many :distributor_shipping_methods, foreign_key: :distributor_id has_many :shipping_methods, through: :distributor_shipping_methods + has_many :customers delegate :latitude, :longitude, :city, :state_name, :to => :address @@ -178,6 +179,10 @@ class Enterprise < ActiveRecord::Base count(distinct: true) end + def activated? + confirmed_at.present? && sells != 'unspecified' + end + def set_producer_property(property_name, property_value) transaction do property = Spree::Property.where(name: property_name).first_or_create!(presentation: property_name) @@ -212,12 +217,16 @@ class Enterprise < ActiveRecord::Base ", self.id, self.id) end + def relatives_including_self + Enterprise.where(id: relatives.pluck(:id) | [id]) + end + def distributors - self.relatives.is_distributor + self.relatives_including_self.is_distributor end def suppliers - self.relatives.is_primary_producer + self.relatives_including_self.is_primary_producer end def website diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index fbdef9d52c..de06a0578e 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -25,6 +25,32 @@ class EnterpriseRelationship < ActiveRecord::Base scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name') + # Load an array of the relatives of each enterprise (ie. any enterprise related to it in + # either direction). This array is split into distributors and producers, and has the format: + # {enterprise_id => {distributors: [id, ...], producers: [id, ...]} } + def self.relatives(activated_only=false) + relationships = EnterpriseRelationship.includes(:child, :parent) + relatives = {} + + relationships.each do |r| + relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new} + relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new} + + if !activated_only || r.child.activated? + relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer + relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor + end + + if !activated_only || r.parent.activated? + relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer + relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor + end + end + + relatives + end + + def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 1013b78c43..515f26621d 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -92,11 +92,25 @@ class OrderCycle < ActiveRecord::Base with_distributor(distributor).soonest_closing.first end - def self.most_recently_closed_for(distributor) with_distributor(distributor).most_recently_closed.first end + # Find the earliest closing times for each distributor in an active order cycle, and return + # them in the format {distributor_id => closing_time, ...} + def self.earliest_closing_times + Hash[ + Exchange. + outgoing. + joins(:order_cycle). + merge(OrderCycle.active). + group('exchanges.receiver_id'). + select('exchanges.receiver_id AS receiver_id, MIN(order_cycles.orders_close_at) AS earliest_close_at'). + map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] } + ] + end + + def clone! oc = self.dup oc.name = "COPY OF #{oc.name}" diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 39899d5991..56478f99c7 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -184,6 +184,8 @@ class AbilityDecorator # Reports page can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + + can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) end diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 836080183c..8447d24046 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -1,9 +1,14 @@ module Spree Adjustment.class_eval do - has_one :metadata, class_name: 'AdjustmentMetadata', dependent: :destroy + # Deletion of metadata is handled in the database. + # So we don't need the option `dependent: :destroy` as long as + # AdjustmentMetadata has no destroy logic itself. + has_one :metadata, class_name: 'AdjustmentMetadata' scope :enterprise_fee, where(originator_type: 'EnterpriseFee') scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') + scope :with_tax, where('spree_adjustments.included_tax > 0') + scope :without_tax, where('spree_adjustments.included_tax = 0') attr_accessible :included_tax diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 4eec0bcd2b..f64f197d56 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -24,6 +24,15 @@ Spree::LineItem.class_eval do where('spree_products.supplier_id IN (?)', enterprises) } + scope :with_tax, joins(:adjustments). + where('spree_adjustments.originator_type = ?', 'Spree::TaxRate'). + select('DISTINCT spree_line_items.*') + + # Line items without a Spree::TaxRate-originated adjustment + scope :without_tax, joins("LEFT OUTER JOIN spree_adjustments ON (spree_adjustments.adjustable_id=spree_line_items.id AND spree_adjustments.adjustable_type = 'Spree::LineItem' AND spree_adjustments.originator_type='Spree::TaxRate')"). + where('spree_adjustments.id IS NULL') + + def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index cbc5c0086d..3c0f212945 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -10,11 +10,14 @@ Spree::Order.class_eval do belongs_to :order_cycle belongs_to :distributor, :class_name => 'Enterprise' belongs_to :cart + belongs_to :customer + validates :customer, presence: true, if: :require_customer? validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? } attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor + before_validation :associate_customer, unless: :customer_is_valid? checkout_flow do go_to_state :address @@ -261,4 +264,24 @@ Spree::Order.class_eval do def product_distribution_for(line_item) line_item.variant.product.product_distribution_for self.distributor end + + def require_customer? + return true unless new_record? or state == 'cart' + end + + def customer_is_valid? + return true unless require_customer? + customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email) + end + + def associate_customer + email_for_customer = user.andand.email || email + existing_customer = Customer.of(distributor).find_by_email(email_for_customer) + if existing_customer + self.customer = existing_customer + else + new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) + self.customer = new_customer + end + end end diff --git a/app/models/spree/shipping_method_decorator.rb b/app/models/spree/shipping_method_decorator.rb index 4a2cf75b47..b8be603048 100644 --- a/app/models/spree/shipping_method_decorator.rb +++ b/app/models/spree/shipping_method_decorator.rb @@ -25,6 +25,22 @@ Spree::ShippingMethod.class_eval do scope :by_name, order('spree_shipping_methods.name ASC') + + # Return the services (pickup, delivery) that different distributors provide, in the format: + # {distributor_id => {pickup: true, delivery: false}, ...} + def self.services + Hash[ + Spree::ShippingMethod. + joins(:distributor_shipping_methods). + group('distributor_id'). + select("distributor_id"). + select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup"). + select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery"). + map { |sm| [sm.distributor_id.to_i, {pickup: sm.pickup == 't', delivery: sm.delivery == 't'}] } + ] + end + + def available_to_order_with_distributor_check?(order, display_on=nil) available_to_order_without_distributor_check?(order, display_on) && self.distributors.include?(order.distributor) diff --git a/app/models/spree/taxon_decorator.rb b/app/models/spree/taxon_decorator.rb index 10ee0b4719..1a26ce73a8 100644 --- a/app/models/spree/taxon_decorator.rb +++ b/app/models/spree/taxon_decorator.rb @@ -9,4 +9,40 @@ Spree::Taxon.class_eval do #fs << Spree::ProductFilters.distributor_filter if Spree::ProductFilters.respond_to? :distributor_filter fs end + + # Find all the taxons of supplied products for each enterprise, indexed by enterprise. + # Format: {enterprise_id => [taxon_id, ...]} + def self.supplied_taxons + taxons = {} + + Spree::Taxon. + joins(:products => :supplier). + select('spree_taxons.*, enterprises.id AS enterprise_id'). + each do |t| + + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end + + taxons + end + + # Find all the taxons of distributed products for each enterprise, indexed by enterprise. + # Format: {enterprise_id => [taxon_id, ...]} + def self.distributed_taxons + taxons = {} + + Spree::Taxon. + joins(:products). + merge(Spree::Product.with_order_cycles_outer). + where('o_exchanges.incoming = ?', false). + select('spree_taxons.*, o_exchanges.receiver_id AS enterprise_id'). + each do |t| + + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end + + taxons + end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index d8ea312e23..13ab56c129 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -1,9 +1,7 @@ Spree.user_class.class_eval do - if method_defined? :send_reset_password_instructions_with_delay - Bugsnag.notify RuntimeError.new "send_reset_password_instructions already handled asyncronously - double-calling results in infinite job loop" - else - handle_asynchronously :send_reset_password_instructions - end + # handle_asynchronously will define send_reset_password_instructions_with_delay. + # If handle_asynchronously is called twice, we get an infinite job loop. + handle_asynchronously :send_reset_password_instructions unless method_defined? :send_reset_password_instructions_with_delay has_many :enterprise_roles, :dependent => :destroy has_many :enterprises, through: :enterprise_roles diff --git a/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface new file mode 100644 index 0000000000..548439b60f --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_footer_scripts']" + += render 'shared/analytics' diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb new file mode 100644 index 0000000000..3cb9518a9f --- /dev/null +++ b/app/serializers/api/admin/customer_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::CustomerSerializer < ActiveModel::Serializer + attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end +end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 532887ae01..44364c4dbc 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -1,4 +1,7 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer + # We reference this here because otherwise the serializer complains about its absence + Api::IdSerializer + def serializable_hash cached_serializer_hash.merge uncached_serializer_hash end @@ -6,11 +9,11 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer private def cached_serializer_hash - Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash + Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash || {} end def uncached_serializer_hash - Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash + Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash || {} end end @@ -18,19 +21,22 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer attributes :orders_close_at, :active def orders_close_at - OrderCycle.first_closing_for(object).andand.orders_close_at + options[:data].earliest_closing_times[object.id] end def active - @options[:active_distributors].andand.include? object + options[:data].active_distributors.andand.include? object end - - end class Api::CachedEnterpriseSerializer < ActiveModel::Serializer cached - delegate :cache_key, to: :object + #delegate :cache_key, to: :object + + def cache_key + object.andand.cache_key + end + attributes :name, :id, :description, :latitude, :longitude, :long_description, :website, :instagram, :linkedin, :twitter, @@ -38,17 +44,27 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer :email, :hash, :logo, :promo_image, :path, :pickup, :delivery, :icon, :icon_font, :producer_icon_font, :category, :producers, :hubs - has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer - has_many :supplied_taxons, serializer: Api::IdSerializer + attributes :taxons, :supplied_taxons has_one :address, serializer: Api::AddressSerializer + + def taxons + ids_to_objs options[:data].distributed_taxons[object.id] + end + + def supplied_taxons + ids_to_objs options[:data].supplied_taxons[object.id] + end + def pickup - object.shipping_methods.where(:require_ship_address => false).present? + services = options[:data].shipping_method_services[object.id] + services ? services[:pickup] : false end def delivery - object.shipping_methods.where(:require_ship_address => true).present? + services = options[:data].shipping_method_services[object.id] + services ? services[:delivery] : false end def email @@ -72,11 +88,13 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer end def producers - ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer}) + relatives = options[:data].relatives[object.id] + relatives ? ids_to_objs(relatives[:producers]) : [] end def hubs - ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer}) + relatives = options[:data].relatives[object.id] + relatives ? ids_to_objs(relatives[:distributors]) : [] end # Map svg icons. @@ -116,4 +134,11 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer } icon_fonts[object.category] end + + + private + + def ids_to_objs(ids) + ids.andand.map { |id| {id: id} } + end end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 0de794796b..4c4aec2310 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -30,8 +30,9 @@ class Api::CachedProductSerializer < ActiveModel::Serializer #cached #delegate :cache_key, to: :object - attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy, - :notes, :description, :properties_with_values + attributes :id, :name, :permalink, :count_on_hand + attributes :on_demand, :group_buy, :notes, :description + attributes :properties_with_values has_many :variants, serializer: Api::VariantSerializer has_many :taxons, serializer: Api::IdSerializer @@ -46,13 +47,11 @@ class Api::CachedProductSerializer < ActiveModel::Serializer end def variants - # We use the in_stock? method here instead of the in_stock scope because we need to - # look up the stock as overridden by VariantOverrides, and the scope method is not affected - # by them. - - object.variants. - for_distribution(options[:current_order_cycle], options[:current_distributor]). - each { |v| v.scope_to_hub options[:current_distributor] }. - select(&:in_stock?) + options[:variants][object.id] || [] end + + def master + options[:master_variants][object.id].andand.first + end + end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml new file mode 100644 index 0000000000..66790a34df --- /dev/null +++ b/app/views/admin/customers/index.html.haml @@ -0,0 +1,72 @@ +- content_for :page_title do + %h1.page-title Customers + += admin_inject_shops + +%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } + .row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } } + .five.columns.alpha + %h3 Please select a Hub: + .four.columns + %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } + .seven.columns.omega   + + .row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' } + .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } + .five.columns.alpha + %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } + .five.columns   + -# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true } + -# %span{ :class => 'icon-check' }   Actions + -# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + -# %div.menu{ 'ng-show' => "expanded" } + -# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true } + -# %span{ :class => 'three columns omega' } {{action.name }} + .three.columns   + .three.columns.omega + %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } + %span{ :class => 'icon-reorder' }   Columns + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } + %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} + %span{ :class => 'two columns omega' } {{column.name }} + .row{ 'ng-if' => 'shop && !loaded()' } + .sixteen.columns.alpha#loading + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING CUSTOMERS + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} + %h1#no_results No customers found. + + + .row{ ng: { show: "loaded() && filteredCustomers.length > 0" } } + %form{ name: "customers" } + %table.index#customers + %col.email{ width: "20%"} + %col.code{ width: "20%"} + %col.tags{ width: "50%"} + %col.actions{ width: "10%"} + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + -# %th.bulk + -# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } + %th.email{ 'ng-show' => 'columns.email.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email + %th.code{ 'ng-show' => 'columns.code.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code + %th.tags{ 'ng-show' => 'columns.tags.visible' } Tags + %th.actions + Ask?  + %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } + -# %td.bulk + -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } + %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} + %td.code{ 'ng-show' => 'columns.code.visible' } + %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } + %td.tags{ 'ng-show' => 'columns.tags.visible' } + .tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"} + %tags_with_translation{ object: 'customer' } + %td.actions + %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } + %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index e957b70b1c..3fe7ba09e9 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,20 +1,22 @@ %h3 = "Hi, #{@resource.contact}!" %p.lead - = "Please confirm your email address for " - %strong - = "#{@resource.name}." + = "A profile for #{@resource.name} has been successfully created!" + To activate your Profile we need to confirm this email address. %p   %p.callout - Click the link below to confirm your email and to activate your enterprise. This link can be used only once: + Please click the link below to confirm your email and to continue setting up your profile. %br %strong = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %p   %p - = "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions." + After confirming your email you can access your administration account for this enterprise. + See the + = link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 5c69caae77..3cd9d14034 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -1,67 +1,27 @@ %h3 = "Welcome, #{@enterprise.contact}!" %p.lead - Congratulations, + Thank you for confirming your email address. %strong - %strong= @enterprise.name + = @enterprise.name = "is now part of #{ Spree::Config.site_name }!" -/ Heading Panel + %p - Please find below all the details for viewing and editing your enterprise on - %strong= "#{ Spree::Config.site_name }." - We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. + The User Guide with detailed support for setting up your Producer or Hub is here: + = link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' --#%p   - --# %p.callout --# %strong --# Your enterprise details --# %table{:width => "100%"} --# %tr --# %td{:align => "right"} --# %strong --# Shop URL --# %td   --# %td --# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"} --# = main_app.enterprise_shop_url(@enterprise) --# %tr --# %td   --# %tr --# %td{:align => "right"} --# %strong --# Email --# %td   --# %td --# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} --# = @enterprise.email - -%p   %p - Log into - %strong= "#{ Spree::Config.site_name } Admin" - in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise! + You can manage your account by logging into the + = link_to 'Admin Panel', spree.admin_url + or by clicking on the cog in the top right hand side of the homepage, and selecting Administration. -%p.callout - %strong - OFN Admin -%table{ :width => "100%"} - %tr - %td{:align => "right"} - %strong - Admin - %td   - %td - %a{:href => "#{ spree.admin_url }", :target => "_blank"} - = spree.admin_url - -%p   -/ /Heading Panel %p - We're so pleased to have you as a valued member of - %strong= "#{Spree::Config.site_name}!" - Don't hestitate to get in touch if you have any questions. + We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. + = link_to 'Join the community.', 'http://community.openfoodnetwork.org/' + +%p + If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out! = render 'shared/mailers/signoff' -= render 'shared/mailers/social_and_contact' \ No newline at end of file += render 'shared/mailers/social_and_contact' diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index 89ce1a30ac..08e25417cb 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -3,7 +3,7 @@ %shop.darkswarm - content_for :order_cycle_form do - %div{"ng-controller" => "OrderCycleChangeCtrl"} + %div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true} %closing{"ng-if" => "OrderCycle.selected()"} Next order closing %strong {{ OrderCycle.orders_close_at() | date_in_words }} diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1bc965dfb3..80101d9e8c 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -3,8 +3,8 @@ = inject_enterprises -# inject enterprises in this group --# further hubs and producers of these enterprises can't be resoleved within this small subset -= inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, active_distributors: @active_distributors +-# further hubs and producers of these enterprises can't be resolved within this small subset += inject_group_enterprises #group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"} .small-12.columns.pad-top @@ -95,7 +95,7 @@ = render partial: 'home/fat' = render partial: 'shared/components/enterprise_no_results' - + .small-12.medium-12.large-3.columns = render partial: 'contact' diff --git a/app/views/json/_current_hub.rabl b/app/views/json/_current_hub.rabl deleted file mode 100644 index 103baf9fb3..0000000000 --- a/app/views/json/_current_hub.rabl +++ /dev/null @@ -1,6 +0,0 @@ -object current_distributor -extends 'json/partials/enterprise' - -child suppliers: :producers do - attributes :id -end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index a627ba4896..5169efa942 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -24,7 +24,7 @@ = render partial: "shared/ie_warning" = javascript_include_tag "iehack" - = inject_json "currentHub", "current_hub" + = inject_current_hub = inject_json "user", "current_user" = inject_json "railsFlash", "flash" = inject_taxons diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index 0e46795701..e3ae9815e2 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -1,5 +1,6 @@ -= inject_enterprises -.producers.pad-top{"ng-controller" => "EnterprisesCtrl"} += inject_enterprises + +.producers.pad-top{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns.pad-top %h1 Find local producers diff --git a/app/views/shared/_analytics.html.haml b/app/views/shared/_analytics.html.haml index ee9ba69923..16ad08ff5f 100644 --- a/app/views/shared/_analytics.html.haml +++ b/app/views/shared/_analytics.html.haml @@ -1,8 +1,9 @@ -:javascript - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +- if Rails.env.production? + :javascript + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-62912229-1', 'auto'); - ga('send', 'pageview'); + ga('create', 'UA-62912229-1', 'auto'); + ga('send', 'pageview'); diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index 5ed57917fd..e5ba9af83b 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -2,7 +2,7 @@ %section.left %a.left-off-canvas-toggle.menu-icon %span - %section.right + %section.right{"ng-cloak" => true} .cart = render partial: "shared/menu/cart" %a{href: main_app.shop_path} @@ -11,34 +11,34 @@ %aside.left-off-canvas-menu.show-for-medium-down %ul.off-canvas-list %li.ofn-logo - %a{href: root_path} + %a{href: root_path} %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - + - if current_page? root_path %li.li-menu %a{"ofn-scroll-to" => "hubs"} - %span.nav-primary + %span.nav-primary %i.ofn-i_040-hub Hubs - else %li.li-menu %a{href: root_path + "#/#hubs"} - %span.nav-primary + %span.nav-primary %i.ofn-i_040-hub Hubs %li.li-menu %a{href: main_app.map_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_037-map Map %li.li-menu %a{href: main_app.producers_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_036-producers Producers %li.li-menu %a{href: main_app.groups_path} - %span.nav-primary + %span.nav-primary %i.ofn-i_035-groups Groups diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 0df4f022aa..bdaded0f0d 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -1,4 +1,4 @@ -%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", +%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} // TODO: Needs an ng-show to slide content down diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index 3641e0cbd5..48c0a1d7b5 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,11 +1,11 @@ -#tabs{"ng-controller" => "TabsCtrl"} +#tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true} .row %tabset -# Build all tabs. - - for name, heading_cols in { about: ["About #{current_distributor.name}", 6], - producers: ["Producers",2], + - for name, heading_cols in { about: ["About #{current_distributor.name}", 6], + producers: ["Producers",2], contact: ["Contact",2], - groups: ["Groups",2]} + groups: ["Groups",2]} -# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl. - heading, cols = heading_cols %tab.columns{heading: heading, diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index caf18c6cd3..71134e38a9 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -103,7 +103,7 @@ %form{ 'ng-model' => "bulk_order_form" } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead - %tr + %tr{ ng: { controller: "ColumnsCtrl" } } %th.bulk %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.order_no{ 'ng-show' => 'columns.order_no.visible' } @@ -132,28 +132,28 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } - %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} - %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} - %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} - %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} - %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} - %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} - %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} - %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} - %td.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} - %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" } - %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "unit_value" } - %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-model' => "line_item.price", 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "price" } - %td.actions - %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } - %td.actions - %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } + %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } + %td.bulk + %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } + %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} + %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} + %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} + %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} + %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} + %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} + %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} + %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} + %td.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} + %td.quantity{ 'ng-show' => 'columns.quantity.visible' } + %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } + %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} + %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } + %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" } + %td.price{ 'ng-show' => 'columns.price.visible' } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } + %td.actions + %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } + %td.actions + %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 6e22aef8ff..7767b8de89 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -17,8 +17,10 @@ %col.actions %thead - %tr + %tr{ ng: { controller: "ColumnsCtrl" } } %th.left-actions + %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' } + Expand All %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer %th.sku{ 'ng-show' => 'columns.sku.visible' } SKU %th.name{ 'ng-show' => 'columns.name.visible' } Name diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index 376e88071a..6ac25ae286 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -1,6 +1,6 @@ %tr.product{ :id => "p_{{product.id}}" } %td.left-actions - %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } + %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } %td.producer{ 'ng-show' => 'columns.producer.visible' } %select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml new file mode 100644 index 0000000000..1ae4e3b279 --- /dev/null +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -0,0 +1,44 @@ += form_for @search, url: spree.xero_invoices_admin_reports_path do |f| + = render 'date_range_form', f: f + + .row + .four.columns.alpha= label_tag nil, "Hub: " + .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) + .row + .four.columns.alpha= label_tag nil, "Order Cycle: " + .four.columns.omega= f.select(:order_cycle_id_eq, + options_for_select(report_order_cycle_options(@order_cycles), params[:q][:order_cycle_id_eq]), + {:include_blank => true}, {:class => "select2 fullwidth"}) + + .row + .four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:" + .twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number] + .row + .four.columns.alpha= label_tag :invoice_date, "Invoice date:" + .twelve.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker' + .row + .four.columns.alpha= label_tag :due_date, "Due date:" + .twelve.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker' + .row + .four.columns.alpha= label_tag :account_code, "Account code:" + .twelve.columns.omega= text_field_tag :account_code, params[:account_code] + .row + .four.columns.alpha= label_tag :csv, "Download as CSV:" + .twelve.columns.omega= check_box_tag :csv + .row + .four.columns.alpha= button t(:search) + + +%table#listing_invoices.index + %thead + %tr + - @report.header.each do |header| + %th= header + %tbody + - @report.table.each do |row| + %tr + - row.each do |column| + %td= column + - if @report.table.empty? + %tr + %td{:colspan => "2"}= t(:none) diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index 82120add82..de142e3cac 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -9,7 +9,7 @@ - else = @order.distributor.next_collection_at - = render "shopping_shared/details" + = render "shopping_shared/details" if current_distributor.present? %fieldset#order_summary{"data-hook" => ""} .row diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index fb52f57119..c85b377fa4 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -21,7 +21,9 @@ %hr/ %p   %p.lead - Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + Thanks for joining the network. + If you are a customer, we look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + If you are a producer or food enterprise, we are excited to have you as a part of the network. %p We welcome all your questions and feedback; you can use the %em diff --git a/config/application.rb b/config/application.rb index 311aab16cb..2461f9b72d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -82,7 +82,7 @@ module Openfoodnetwork config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.0' + config.assets.version = '1.1' config.sass.load_paths += [ "#{Gem.loaded_specs['foundation-rails'].full_gem_path}/vendor/assets/stylesheets/foundation/components", diff --git a/config/database.yml b/config/database.yml index d74ed6256a..7eef2396f9 100644 --- a/config/database.yml +++ b/config/database.yml @@ -10,7 +10,7 @@ development: test: adapter: postgresql encoding: unicode - database: open_food_network_test + database: open_food_network_test<%= ENV['TEST_ENV_NUMBER'] %> pool: 5 host: localhost username: ofn diff --git a/config/environments/development.rb b/config/environments/development.rb index efa229e33c..200484122a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -31,7 +31,7 @@ Openfoodnetwork::Application.configure do # Show emails using Letter Opener config.action_mailer.delivery_method = :letter_opener - config.action_mailer.default_url_options = { host: "test.com" } + config.action_mailer.default_url_options = { host: "0.0.0.0:3000" } end diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb index 8fc9aa8ec7..80dc11d3aa 100644 --- a/config/initializers/delayed_job.rb +++ b/config/initializers/delayed_job.rb @@ -2,6 +2,10 @@ Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) Delayed::Worker.destroy_failed_jobs = false Delayed::Worker.max_run_time = 15.minutes +# Uncomment the next line if you want jobs to be executed straight away. +# For example you want emails to be opened in your browser while testing. +#Delayed::Worker.delay_jobs = false + # Notify bugsnag when a job fails # Code adapted from http://trevorturk.com/2011/01/25/notify-hoptoad-if-theres-an-exception-in-delayedjob/ class Delayed::Worker diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 721e495265..d301b88739 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,10 @@ # Be sure to restart your server when you modify this file. -Openfoodnetwork::Application.config.session_store :cookie_store, key: '_openfoodnetwork_session' +# The cookie_store can be too small for very long URLs stored by Devise. +# The maximum size of cookies is 4096 bytes. +#Openfoodnetwork::Application.config.session_store :cookie_store, key: '_openfoodnetwork_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") -# Openfoodnetwork::Application.config.session_store :active_record_store +Openfoodnetwork::Application.config.session_store :active_record_store diff --git a/config/routes.rb b/config/routes.rb index 54bda534b6..8e62cc3193 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,6 +83,8 @@ Openfoodnetwork::Application.routes.draw do resources :variant_overrides do post :bulk_update, on: :collection end + + resources :customers, only: [:index, :update] end namespace :api do @@ -138,6 +140,7 @@ Spree::Core::Engine.routes.prepend do match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" 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/reports/xero_invoices' => 'admin/reports#xero_invoices', :as => "xero_invoices_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 diff --git a/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb b/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb old mode 100755 new mode 100644 diff --git a/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb b/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..6bbd5594ea --- /dev/null +++ b/db/migrate/20150508030520_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb @@ -0,0 +1,31 @@ +# This migration comes from acts_as_taggable_on_engine (originally 1) +class ActsAsTaggableOnMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.string :name + end + + create_table :taggings do |t| + t.references :tag + + # You should make sure that the column created is + # long enough to store the required class names. + t.references :taggable, polymorphic: true + t.references :tagger, polymorphic: true + + # Limit is created to prevent MySQL error on index + # length for MyISAM table type: http://bit.ly/vgW2Ql + t.string :context, limit: 128 + + t.datetime :created_at + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..4ca676f6c7 --- /dev/null +++ b/db/migrate/20150508030521_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -0,0 +1,20 @@ +# This migration comes from acts_as_taggable_on_engine (originally 2) +class AddMissingUniqueIndices < ActiveRecord::Migration + def self.up + add_index :tags, :name, unique: true + + remove_index :taggings, :tag_id + remove_index :taggings, [:taggable_id, :taggable_type, :context] + add_index :taggings, + [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], + unique: true, name: 'taggings_idx' + end + + def self.down + remove_index :tags, :name + + remove_index :taggings, name: 'taggings_idx' + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..8edb508078 --- /dev/null +++ b/db/migrate/20150508030522_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from acts_as_taggable_on_engine (originally 3) +class AddTaggingsCounterCacheToTags < ActiveRecord::Migration + def self.up + add_column :tags, :taggings_count, :integer, default: 0 + + ActsAsTaggableOn::Tag.reset_column_information + ActsAsTaggableOn::Tag.find_each do |tag| + ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) + end + end + + def self.down + remove_column :tags, :taggings_count + end +end diff --git a/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..71f2d7f433 --- /dev/null +++ b/db/migrate/20150508030523_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 4) +class AddMissingTaggableIndex < ActiveRecord::Migration + def self.up + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + remove_index :taggings, [:taggable_id, :taggable_type, :context] + end +end diff --git a/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb new file mode 100644 index 0000000000..bfb06bc7cd --- /dev/null +++ b/db/migrate/20150508030524_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -0,0 +1,10 @@ +# This migration comes from acts_as_taggable_on_engine (originally 5) +# This migration is added to circumvent issue #623 and have special characters +# work properly +class ChangeCollationForTagNames < ActiveRecord::Migration + def up + if ActsAsTaggableOn::Utils.using_mysql? + execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") + end + end +end diff --git a/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb b/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb new file mode 100644 index 0000000000..deeacbc608 --- /dev/null +++ b/db/migrate/20150508072454_remove_customer_code_not_null_constraint.rb @@ -0,0 +1,9 @@ +class RemoveCustomerCodeNotNullConstraint < ActiveRecord::Migration + def up + change_column :customers, :code, :string, null: true + end + + def down + change_column :customers, :code, :string, null: false + end +end diff --git a/db/migrate/20150508072938_add_customer_to_orders.rb b/db/migrate/20150508072938_add_customer_to_orders.rb new file mode 100644 index 0000000000..79d69baf49 --- /dev/null +++ b/db/migrate/20150508072938_add_customer_to_orders.rb @@ -0,0 +1,16 @@ +class AddCustomerToOrders < ActiveRecord::Migration + def change + add_column :spree_orders, :customer_id, :integer + add_index :spree_orders, :customer_id + add_foreign_key :spree_orders, :customers, column: :customer_id + + Spree::Order.where("spree_orders.email IS NOT NULL AND distributor_id IS NOT NULL AND customer_id IS NULL").each do |order| + customer = Customer.find_by_email_and_enterprise_id(order.email, order.distributor_id) + unless customer.present? + user = Spree::User.find_by_email(order.email) + customer = Customer.create!(email: order.email, enterprise_id: order.distributor_id, user_id: user.andand.id ) + end + order.update_attribute(:customer, customer) + end + end +end diff --git a/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb new file mode 100644 index 0000000000..e8841b2c5f --- /dev/null +++ b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb @@ -0,0 +1,16 @@ +class AddUniqueIndexToEnterprisePermalink < ActiveRecord::Migration + def change + duplicates = Enterprise.group(:permalink).having('count(*) > 1').pluck(:permalink) + duplicates.each { |p| resolve_permalink(p) }; + add_index :enterprises, :permalink, :unique => true + end + + def resolve_permalink(permalink) + conflicting = Enterprise.where(permalink: permalink) + while conflicting.size > 1 do + enterprise = conflicting.pop + enterprise.permalink = nil + enterprise.save + end + end +end diff --git a/db/migrate/20150604045725_add_sessions_table.rb b/db/migrate/20150604045725_add_sessions_table.rb new file mode 100644 index 0000000000..4c879564a5 --- /dev/null +++ b/db/migrate/20150604045725_add_sessions_table.rb @@ -0,0 +1,12 @@ +class AddSessionsTable < ActiveRecord::Migration + def change + create_table :sessions do |t| + t.string :session_id, :null => false + t.text :data + t.timestamps + end + + add_index :sessions, :session_id + add_index :sessions, :updated_at + end +end diff --git a/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb b/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb new file mode 100644 index 0000000000..ee9d8d4af8 --- /dev/null +++ b/db/migrate/20150605052516_dependent_delete_adjustment_metadata.rb @@ -0,0 +1,11 @@ +class DependentDeleteAdjustmentMetadata < ActiveRecord::Migration + def up + remove_foreign_key "adjustment_metadata", name: "adjustment_metadata_adjustment_id_fk" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete + end + + def down + remove_foreign_key "adjustment_metadata", name: "adjustment_metadata_adjustment_id_fk" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" + end +end diff --git a/db/schema.rb b/db/schema.rb index e903d7a8c3..48309e3fd7 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 => 20150527004427) do +ActiveRecord::Schema.define(:version => 20150605052516) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -158,7 +158,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do create_table "customers", :force => true do |t| t.string "email", :null => false t.integer "enterprise_id", :null => false - t.string "code", :null => false + t.string "code" t.integer "user_id" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false @@ -323,6 +323,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "enterprises", ["confirmation_token"], :name => "index_enterprises_on_confirmation_token", :unique => true add_index "enterprises", ["is_primary_producer", "sells"], :name => "index_enterprises_on_is_primary_producer_and_sells" add_index "enterprises", ["owner_id"], :name => "index_enterprises_on_owner_id" + add_index "enterprises", ["permalink"], :name => "index_enterprises_on_permalink", :unique => true add_index "enterprises", ["sells"], :name => "index_enterprises_on_sells" create_table "exchange_fees", :force => true do |t| @@ -398,6 +399,16 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "product_distributions", ["enterprise_fee_id"], :name => "index_product_distributions_on_enterprise_fee_id" add_index "product_distributions", ["product_id"], :name => "index_product_distributions_on_product_id" + create_table "sessions", :force => true do |t| + t.string "session_id", :null => false + t.text "data" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" + add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at" + create_table "spree_activators", :force => true do |t| t.string "description" t.datetime "expires_at" @@ -623,12 +634,14 @@ ActiveRecord::Schema.define(:version => 20150527004427) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" + t.integer "customer_id" end + add_index "spree_orders", ["customer_id"], :name => "index_spree_orders_on_customer_id" add_index "spree_orders", ["number"], :name => "index_orders_on_number" create_table "spree_payment_methods", :force => true do |t| @@ -1085,6 +1098,26 @@ ActiveRecord::Schema.define(:version => 20150527004427) do t.integer "state_id" end + create_table "taggings", :force => true do |t| + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" + t.string "context", :limit => 128 + t.datetime "created_at" + end + + add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], :name => "taggings_idx", :unique => true + add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context" + + create_table "tags", :force => true do |t| + t.string "name" + t.integer "taggings_count", :default => 0 + end + + add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true + create_table "variant_overrides", :force => true do |t| t.integer "variant_id", :null => false t.integer "hub_id", :null => false @@ -1095,7 +1128,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" - add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete add_foreign_key "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" @@ -1190,6 +1223,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_foreign_key "spree_option_values_variants", "spree_variants", name: "spree_option_values_variants_variant_id_fk", column: "variant_id" add_foreign_key "spree_orders", "carts", name: "spree_orders_cart_id_fk" + add_foreign_key "spree_orders", "customers", name: "spree_orders_customer_id_fk" add_foreign_key "spree_orders", "enterprises", name: "spree_orders_distributor_id_fk", column: "distributor_id" add_foreign_key "spree_orders", "order_cycles", name: "spree_orders_order_cycle_id_fk" add_foreign_key "spree_orders", "spree_addresses", name: "spree_orders_bill_address_id_fk", column: "bill_address_id" diff --git a/lib/open_food_network/enterprise_injection_data.rb b/lib/open_food_network/enterprise_injection_data.rb new file mode 100644 index 0000000000..87516007c6 --- /dev/null +++ b/lib/open_food_network/enterprise_injection_data.rb @@ -0,0 +1,27 @@ +module OpenFoodNetwork + class EnterpriseInjectionData + def active_distributors + @active_distributors ||= Enterprise.distributors_with_active_order_cycles + end + + def earliest_closing_times + @earliest_closing_times ||= OrderCycle.earliest_closing_times + end + + def shipping_method_services + @shipping_method_services ||= Spree::ShippingMethod.services + end + + def relatives + @relatives ||= EnterpriseRelationship.relatives(true) + end + + def supplied_taxons + @supplied_taxons ||= Spree::Taxon.supplied_taxons + end + + def distributed_taxons + @distributed_taxons ||= Spree::Taxon.distributed_taxons + end + end +end diff --git a/lib/open_food_network/order_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb index 2662b176dd..011e8d19fe 100644 --- a/lib/open_food_network/order_and_distributor_report.rb +++ b/lib/open_food_network/order_and_distributor_report.rb @@ -1,4 +1,3 @@ - module OpenFoodNetwork class OrderAndDistributorReport @@ -8,14 +7,15 @@ module OpenFoodNetwork def header ["Order date", "Order Id", - "Customer Name","Customer Email", "Customer Phone", "Customer City", - "SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost", - "Payment method", - "Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"] + "Customer Name","Customer Email", "Customer Phone", "Customer City", + "SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost", + "Payment method", + "Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"] end def table order_and_distributor_details = [] + @orders.each do |order| order.line_items.each do |line_item| order_and_distributor_details << [order.created_at, order.id, @@ -25,6 +25,7 @@ module OpenFoodNetwork order.distributor.andand.name, order.distributor.address.address1, order.distributor.address.city, order.distributor.address.zipcode, order.special_instructions ] end end + order_and_distributor_details end end diff --git a/lib/open_food_network/order_cycle_permissions.rb b/lib/open_food_network/order_cycle_permissions.rb index 08fe7b2ee4..f728348630 100644 --- a/lib/open_food_network/order_cycle_permissions.rb +++ b/lib/open_food_network/order_cycle_permissions.rb @@ -128,6 +128,8 @@ module OpenFoodNetwork producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub) + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break # TODO: Remove this when all P-OC are sorted out active_variants = [] @@ -135,7 +137,7 @@ module OpenFoodNetwork active_variants = exchange.variants end - Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants) else # Any variants produced by MY PRODUCERS that are in this order cycle, where my producer has granted P-OC to the hub producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: managed_participating_producers) @@ -165,6 +167,8 @@ module OpenFoodNetwork producers = related_enterprises_granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + hub_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', hub) + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break # TODO: Remove this when all P-OC are sorted out active_variants = [] @@ -172,7 +176,7 @@ module OpenFoodNetwork active_variants = exchange.variants end - Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + Spree::Variant.where(id: coordinator_variants | hub_variants | permitted_variants | active_variants) else # Any of my managed producers in this order cycle granted P-OC by the hub granted_producers = related_enterprises_granted(:add_to_order_cycle, by: [hub], scope: managed_participating_producers) diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 163f9a4f50..2b797bd944 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -47,7 +47,7 @@ module OpenFoodNetwork end def variants - filter(child_variants) + filter(master_variants) + filter(child_variants) end def child_variants @@ -57,16 +57,6 @@ module OpenFoodNetwork .order("spree_products.name") end - def master_variants - Spree::Variant.where(:is_master => true) - .joins(:product) - .where("(select spree_variants.id from spree_variants as other_spree_variants - WHERE other_spree_variants.product_id = spree_variants.product_id - AND other_spree_variants.is_master = 'f' LIMIT 1) IS NULL") - .merge(visible_products) - .order("spree_products.name") - end - def filter(variants) # NOTE: Ordering matters. # filter_to_order_cycle and filter_to_distributor return Arrays not Arel @@ -107,7 +97,7 @@ module OpenFoodNetwork def filter_to_order_cycle(variants) if params[:order_cycle_id].to_i > 0 order_cycle = OrderCycle.find params[:order_cycle_id] - variants.select! { |v| order_cycle.variants.include? v } + variants.select { |v| order_cycle.variants.include? v } else variants end diff --git a/lib/open_food_network/referer_parser.rb b/lib/open_food_network/referer_parser.rb new file mode 100644 index 0000000000..b90ef21829 --- /dev/null +++ b/lib/open_food_network/referer_parser.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork + class RefererParser + def self.path(referer) + parse_uri(referer).andand.path if referer + end + + def self.parse_uri(string) + begin + # TODO: make this operation obsolete by fixing URLs generated by AngularJS + string.sub!('##', '#') + URI(string) + rescue URI::InvalidURIError + nil + end + end + end +end diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb new file mode 100644 index 0000000000..1a7f7bd636 --- /dev/null +++ b/lib/open_food_network/xero_invoices_report.rb @@ -0,0 +1,104 @@ +module OpenFoodNetwork + class XeroInvoicesReport + def initialize(orders, opts={}) + @orders = orders + + @opts = opts. + reject { |k, v| v.blank? }. + reverse_merge({invoice_date: Date.today, + due_date: 2.weeks.from_now.to_date, + account_code: 'food sales'}) + end + + def header + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) + end + + def table + rows = [] + + @orders.each_with_index do |order, i| + invoice_number = invoice_number_for(order, i) + rows += rows_for_order(order, invoice_number, @opts) + end + + rows + end + + + private + + def invoice_number_for(order, i) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + end + + def rows_for_order(order, invoice_number, opts) + [ + summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), + summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts) + ].compact + end + + def summary_row(order, description, amount, invoice_number, tax_type, opts={}) + return nil if amount == 0 + + [order.bill_address.full_name, + order.email, + order.bill_address.address1, + order.bill_address.address2, + '', + '', + order.bill_address.city, + order.bill_address.state, + order.bill_address.zipcode, + order.bill_address.country.andand.name, + invoice_number, + order.number, + opts[:invoice_date], + opts[:due_date], + '', + description, + '1', + amount, + '', + opts[:account_code], + tax_type, + '', + '', + '', + '', + Spree::Config.currency, + '', + order.paid? ? 'Y' : 'N' + ] + end + + def total_untaxable_products(order) + order.line_items.without_tax.sum &:amount + end + + def total_taxable_products(order) + order.line_items.with_tax.sum &:amount + end + + def total_untaxable_fees(order) + order.adjustments.enterprise_fee.without_tax.sum &:amount + end + + def total_taxable_fees(order) + order.adjustments.enterprise_fee.with_tax.sum &:amount + end + + def total_shipping(order) + order.adjustments.shipping.sum &:amount + end + + def tax_on_shipping_s(order) + tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 + tax_on_shipping ? 'GST on Income' : 'GST Free Income' + end + end +end diff --git a/script/ci/merge_branch_to_master.sh b/script/ci/merge_branch_to_master.sh index 3eaae17d56..fd9c602802 100755 --- a/script/ci/merge_branch_to_master.sh +++ b/script/ci/merge_branch_to_master.sh @@ -6,5 +6,9 @@ source ./script/ci/includes.sh echo "--- Verifying branch is based on current master" exit_unless_master_merged -echo "--- Pushing branch" -git push origin $BUILDKITE_COMMIT:master +echo "--- Merging and pushing branch" +git checkout master +git merge origin/master +git merge origin/$BUILDKITE_BRANCH +git push origin master +git checkout origin/$BUILDKITE_BRANCH diff --git a/script/ci/run_tests.sh b/script/ci/run_tests.sh index 5539935a8b..9f9cdfa30d 100755 --- a/script/ci/run_tests.sh +++ b/script/ci/run_tests.sh @@ -13,7 +13,8 @@ echo "--- Bundling" bundle install echo "--- Loading test database" -bundle exec rake db:test:load +bundle exec rake db:drop db:create db:schema:load +bundle exec rake parallel:drop parallel:create parallel:load_schema echo "--- Running tests" -bundle exec rspec spec +bundle exec rake parallel:spec diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb new file mode 100644 index 0000000000..bb2e4888c2 --- /dev/null +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Admin::CustomersController, type: :controller do + include AuthenticationWorkflow + + describe "index" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + context "html" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "returns an empty @collection" do + spree_get :index, format: :html + expect(assigns(:collection)).to eq [] + end + end + + context "json" do + let!(:customer) { create(:customer, enterprise: enterprise) } + + context "where I manage the enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + context "and enterprise_id is given in params" do + let(:params) { { format: :json, enterprise_id: enterprise.id } } + + it "scopes @collection to customers of that enterprise" do + spree_get :index, params + expect(assigns(:collection)).to eq [customer] + end + + it "serializes the data" do + expect(ActiveModel::ArraySerializer).to receive(:new) + spree_get :index, params + end + end + + context "and enterprise_id is not given in params" do + it "returns an empty collection" do + spree_get :index, format: :json + expect(assigns(:collection)).to eq [] + end + end + end + + context "and I do not manage the enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "returns an empty collection" do + spree_get :index, format: :json + expect(assigns(:collection)).to eq [] + end + end + end + end + + describe "update" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + context "json" do + let!(:customer) { create(:customer, enterprise: enterprise) } + + context "where I manage the customer's enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "allows me to update the customer" do + spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' } + expect(assigns(:customer)).to eq customer + expect(customer.reload.email).to eq 'new.email@gmail.com' + end + end + + context "where I don't manage the customer's enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "prevents me from updating the customer" do + spree_put :update, format: :json, id: customer.id, customer: { email: 'new.email@gmail.com' } + expect(response).to redirect_to spree.unauthorized_path + expect(assigns(:customer)).to eq nil + expect(customer.email).to_not eq 'new.email@gmail.com' + end + end + end + end +end diff --git a/spec/controllers/base_controller_spec.rb b/spec/controllers/base_controller_spec.rb index 1040b0594c..b5ef006c5b 100644 --- a/spec/controllers/base_controller_spec.rb +++ b/spec/controllers/base_controller_spec.rb @@ -24,9 +24,4 @@ describe BaseController do response.should redirect_to root_url flash[:info].should == "The order cycle you've selected has just closed. Please try again!" end - - it "loads active_distributors" do - Enterprise.stub(:distributors_with_active_order_cycles) { 'active distributors' } - controller.load_active_distributors.should == 'active distributors' - end end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index bbbff9a7b1..924462741f 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -9,21 +9,9 @@ describe HomeController do Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } end - it "sets active distributors" do - get :index - assigns[:active_distributors].should == [distributor] - end - # Exclusion from actual rendered view handled in features/consumer/home it "shows JSON for invisible hubs" do get :index response.body.should have_content invisible_distributor.name end - - # This is done inside the json/hubs Serializer - it "gets the next order cycle for each hub" do - OrderCycle.should_receive(:first_closing_for).twice - get :index - end end - diff --git a/spec/controllers/map_controller_spec.rb b/spec/controllers/map_controller_spec.rb deleted file mode 100644 index fab9b7ac22..0000000000 --- a/spec/controllers/map_controller_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe MapController do - it "loads active distributors" do - active_distributors = double(:distributors) - - Enterprise.stub(:distributors_with_active_order_cycles) { active_distributors } - - get :index - - assigns(:active_distributors).should == active_distributors - end -end diff --git a/spec/controllers/producers_controller_spec.rb b/spec/controllers/producers_controller_spec.rb deleted file mode 100644 index ec3c39036c..0000000000 --- a/spec/controllers/producers_controller_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe ProducersController do - let!(:distributor) { create(:distributor_enterprise) } - - before do - Enterprise.stub(:distributors_with_active_order_cycles) { [distributor] } - Enterprise.stub(:all).and_return([distributor]) - end - - it "sets active distributors" do - get :index - assigns[:active_distributors].should == [distributor] - end -end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 3fdc1a7ea9..68f09b78de 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -176,4 +176,18 @@ describe ShopController do end end end + + describe "loading variants" do + let(:hub) { create(:distributor_enterprise) } + let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } + let(:p) { create(:simple_product) } + let!(:v1) { create(:variant, product: p, unit_value: 3) } + let!(:v2) { create(:variant, product: p, unit_value: 5) } + + it "scopes variants to distribution" do + controller.stub(:current_order_cycle) { oc } + controller.stub(:current_distributor) { hub } + controller.send(:variants_for_shop_by_id).should == {p.id => [v1]} + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index d0a1c897ad..db2b3be299 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -214,7 +214,7 @@ FactoryGirl.define do factory :customer, :class => Customer do email { Faker::Internet.email } enterprise - code 'abc123' + code { Faker::Lorem.word } user end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 4c52ec2256..f5134f4e81 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -205,8 +205,9 @@ feature %q{ expect(page).to have_selector "a.edit-variant", count: 1 # When I remove two, they should be removed - page.all('a.delete-variant').first.click - page.all('a.delete-variant').first.click + page.all('a.delete-variant', visible: true).first.click + expect(page).to have_selector "tr.variant", count: 2 + page.all('a.delete-variant', visible: true).first.click expect(page).to have_selector "tr.variant", count: 1 # When I fill out variant details and hit update diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb new file mode 100644 index 0000000000..c84f8f35ed --- /dev/null +++ b/spec/features/admin/customers_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +feature 'Customers' do + include AuthenticationWorkflow + include WebHelper + + context "as an enterprise user" do + let(:user) { create_enterprise_user } + let(:managed_distributor) { create(:distributor_enterprise, owner: user) } + let(:unmanaged_distributor) { create(:distributor_enterprise) } + + describe "using the customers index" do + let!(:customer1) { create(:customer, enterprise: managed_distributor) } + let!(:customer2) { create(:customer, enterprise: managed_distributor) } + let!(:customer3) { create(:customer, enterprise: unmanaged_distributor) } + + before do + quick_login_as user + visit admin_customers_path + end + + it "passes the smoke test", js: true do + # Prompts for a hub for a list of my managed enterprises + expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] + + select2_select managed_distributor.name, from: "shop_id" + + # Loads the right customers + expect(page).to have_selector "tr#c_#{customer1.id}" + expect(page).to have_selector "tr#c_#{customer2.id}" + expect(page).to_not have_selector "tr#c_#{customer3.id}" + + # Searching + fill_in "quick_search", with: customer2.email + expect(page).to_not have_selector "tr#c_#{customer1.id}" + expect(page).to have_selector "tr#c_#{customer2.id}" + fill_in "quick_search", with: "" + + # Toggling columns + expect(page).to have_selector "th.email" + expect(page).to have_content customer1.email + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Email").click + expect(page).to_not have_selector "th.email" + expect(page).to_not have_content customer1.email + end + + it "allows updating of attributes", js: true do + select2_select managed_distributor.name, from: "shop_id" + + within "tr#c_#{customer1.id}" do + fill_in "code", with: "new-customer-code" + expect(page).to have_css "input#code.update-pending" + end + within "tr#c_#{customer1.id}" do + find(:css, "tags-input .tags input").set "awesome\n" + expect(page).to have_css ".tag_watcher.update-pending" + end + click_button "Update" + + # Every says it updated + expect(page).to have_css "input#code.update-success" + expect(page).to have_css ".tag_watcher.update-success" + + # And it actually did + expect(customer1.reload.code).to eq "new-customer-code" + expect(customer1.tag_list).to eq ["awesome"] + end + end + end +end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 5d72705d8a..4f820bb004 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -300,4 +300,94 @@ feature %q{ ].sort end end + + describe "Xero invoices report" do + let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:user1) { create_enterprise_user enterprises: [distributor1] } + let(:user2) { create_enterprise_user enterprises: [distributor2] } + let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) } + let(:enterprise_fee1) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 10)) } + let(:enterprise_fee2) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 20)) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee1, enterprise_fee2], distributors: [distributor1], variants: [product1.master]) } + + let!(:zone) { create(:zone_with_member) } + let(:country) { Spree::Country.find Spree::Config.default_country_id } + let(:bill_address) { create(:address, firstname: 'Customer', lastname: 'Name', address1: 'customer l1', address2: '', city: 'customer city', zipcode: 1234, country: country) } + let(:order1) { create(:order, order_cycle: order_cycle, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: bill_address) } + let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0) } + let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2) } + + let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } + let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } + + let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", originator: shipping_method, amount: 100.55, included_tax: 10.06) } + let!(:adj_fee1) { create(:adjustment, adjustable: order1, originator: enterprise_fee1, label: "Enterprise fee untaxed", amount: 10, included_tax: 0) } + let!(:adj_fee2) { create(:adjustment, adjustable: order1, originator: enterprise_fee2, label: "Enterprise fee taxed", amount: 20, included_tax: 2) } + + + before do + order1.update_attribute :email, 'customer@email.com' + Timecop.travel(Time.zone.local(2015, 4, 25, 14, 0, 0)) { order1.finalize! } + + login_to_admin_section + click_link 'Reports' + + click_link 'Xero Invoices' + end + + around do |example| + Timecop.travel(Time.zone.local(2015, 4, 26, 14, 0, 0)) do + example.yield + end + end + + it "shows Xero invoices report" do + xero_invoice_table.should match_table [ + xero_invoice_header, + xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), + xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), + xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), + xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income') + ] + end + + it "can customise a number of fields" do + fill_in 'initial_invoice_number', with: '5' + fill_in 'invoice_date', with: '2015-02-12' + fill_in 'due_date', with: '2015-03-12' + fill_in 'account_code', with: 'abc123' + click_button 'Search' + + opts = {invoice_number: '5', invoice_date: '2015-02-12', due_date: '2015-03-12', account_code: 'abc123'} + + xero_invoice_table.should match_table [ + xero_invoice_header, + xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), + xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), + xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), + xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), + xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) + ] + end + + + private + + def xero_invoice_table + find("table#listing_invoices") + end + + def xero_invoice_header + %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) + end + + def xero_invoice_row(description, amount, tax_type, opts={}) + opts.reverse_merge!({invoice_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'}) + + ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, '', 'N'] + + end + end end diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 25a91ae160..27b748cce5 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -45,7 +45,7 @@ feature "Registration", js: true do # Choosing a type expect(page).to have_content 'Last step to add My Awesome Enterprise!' click_link 'producer-panel' - click_button 'Continue' + click_button 'Create Profile' # Enterprise should be created expect(page).to have_content 'Nice one!' diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 96d9279ef5..362ca6479b 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -4,7 +4,7 @@ describe InjectionHelper do let!(:enterprise) { create(:distributor_enterprise, facebook: "roger") } it "will inject via AMS" do - helper.inject_json_ams("test", [enterprise], Api::EnterpriseSerializer).should match enterprise.name + helper.inject_json_ams("test", [enterprise], Api::IdSerializer).should match /#{enterprise.id}/ end it "injects enterprises" do diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 10db2226a4..07202027b4 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -7,6 +7,7 @@ //= require angularjs-file-upload //= require lodash.underscore.js //= require angular-flash.min.js +//= require shared/ng-tags-input.min.js //= require shared/mm-foundation-tpls-0.2.2.min.js //= require textAngular.min.js //= require textAngular-sanitize.min.js diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee new file mode 100644 index 0000000000..22777a6528 --- /dev/null +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -0,0 +1,25 @@ +describe "CustomersCtrl", -> + ctrl = null + scope = null + Customers = null + + beforeEach -> + shops = "list of shops" + + module('admin.customers') + inject ($controller, $rootScope, _Customers_) -> + scope = $rootScope + Customers = _Customers_ + ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops} + + describe "setting the shop on scope", -> + beforeEach -> + spyOn(Customers, "index").andReturn "list of customers" + scope.$apply -> + scope.shop = {id: 1} + + it "calls Customers#index with the correct params", -> + expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1}) + + it "resets $scope.customers with the result of Customers#index", -> + expect(scope.customers).toEqual "list of customers" diff --git a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee new file mode 100644 index 0000000000..7123055d63 --- /dev/null +++ b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee @@ -0,0 +1,31 @@ +describe "Customers service", -> + Customers = CustomerResource = customers = $httpBackend = null + + beforeEach -> + module 'admin.customers' + + inject ($q, _$httpBackend_, _Customers_, _CustomerResource_) -> + Customers = _Customers_ + CustomerResource = _CustomerResource_ + $httpBackend = _$httpBackend_ + $httpBackend.expectGET('/admin/customers.json?enterprise_id=2').respond 200, [{ id: 5, email: 'someone@email.com'}] + + describe "#index", -> + result = null + + beforeEach -> + expect(Customers.loaded).toBe false + result = Customers.index(enterprise_id: 2) + $httpBackend.flush() + + it "stores returned data in @customers, with ids as keys", -> + # This is super weird and freaking annoying. I think resource results have extra + # properties ($then, $promise) that cause them to not be equal to the reponse object + # provided to the expectGET clause above. + expect(Customers.customers).toEqual [ new CustomerResource({ id: 5, email: 'someone@email.com'}) ] + + it "returns @customers", -> + expect(result).toEqual Customers.customers + + it "sets @loaded to true", -> + expect(Customers.loaded).toBe true diff --git a/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee new file mode 100644 index 0000000000..5fd79e71bf --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/controllers/columns_controller_spec.js.coffee @@ -0,0 +1,17 @@ +describe "ColumnsCtrl", -> + ctrl = null + scope = null + Columns = null + + beforeEach -> + Columns = { columns: { name: { visible: true} } } + + module('admin.indexUtils') + inject ($controller, $rootScope) -> + scope = $rootScope + ctrl = $controller 'ColumnsCtrl', {$scope: scope, Columns: Columns} + + it "initialises data", -> + expect(scope.columns).toEqual Columns.columns + expect(scope.predicate).toEqual "" + expect(scope.reverse).toEqual false diff --git a/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee new file mode 100644 index 0000000000..0b6093f31d --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee @@ -0,0 +1,15 @@ +describe "Columns service", -> + Columns = null + + beforeEach -> + module 'admin.indexUtils' + + inject (_Columns_) -> + Columns = _Columns_ + + Columns.columns = ["something"] + + describe "setting columns", -> + it "sets resets @columns and copies each column of the provided object across", -> + Columns.setColumns({ name: { visible: true } }) + expect(Columns.columns).toEqual { name: { visible: true } } diff --git a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee new file mode 100644 index 0000000000..31b85df217 --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee @@ -0,0 +1,149 @@ +describe "Pending Changes", -> + resourcesMock = pendingChanges = null + + beforeEach -> + + resourcesMock = + update: jasmine.createSpy('update').andCallFake (change) -> + $promise: + then: (successFn, errorFn) -> + return successFn({propertyName: "new_value"}) if change.success + errorFn("error") + + module 'admin.indexUtils', ($provide) -> + $provide.value 'resources', resourcesMock + return + + inject (_pendingChanges_) -> + pendingChanges = _pendingChanges_ + + + describe "adding a new change", -> + it "adds a new object with key of id if it does not already exist", -> + expect(pendingChanges.pendingChanges).toEqual {} + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + + it "adds a new object with key of the altered attribute name if it does not already exist", -> + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } + + it "replaces the existing object when adding a change to an attribute which already exists", -> + pendingChanges.add 1, "propertyName", { a: 1 } + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } + pendingChanges.add 1, "propertyName", { b: 2 } + expect(pendingChanges.pendingChanges["1"]["propertyName"]).toEqual { b: 2 } + + it "adds an attribute to key to a line item object when one already exists", -> + pendingChanges.add 1, "propertyName1", { a: 1 } + pendingChanges.add 1, "propertyName2", { b: 2 } + expect(pendingChanges.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } } + + describe "removing all existing changes", -> + it "resets pendingChanges object", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName2"]).toBeDefined() + pendingChanges.removeAll() + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + expect(pendingChanges.pendingChanges).toEqual {} + + describe "removing an existing change", -> + it "deletes a change if it exists", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).toBeDefined() + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).not.toBeDefined() + + it "deletes a line item object if it is empty", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).not.toBeDefined() + + it "does nothing if key with specified attribute does not exist", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 1, "propertyName2" + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 } + + it "does nothing if key with specified id does not exist", -> + pendingChanges.pendingChanges = { 1: { "propertyName1": { a: 1 } } } + expect(pendingChanges.pendingChanges["1"]["propertyName1"]).toBeDefined() + pendingChanges.remove 2, "propertyName1" + expect(pendingChanges.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } } + + describe "submitting an individual change to the server", -> + change = null + beforeEach -> + object = {id: 1} + scope = { reset: jasmine.createSpy('reset'), success: jasmine.createSpy('success'), error: jasmine.createSpy('error') }; + attr = "propertyName" + change = { object: object, scope: scope, attr: attr } + + + it "sends the correct object to dataSubmitter", -> + pendingChanges.submit change + expect(resourcesMock.update.calls.length).toEqual 1 + expect(resourcesMock.update).toHaveBeenCalledWith change + + describe "successful request", -> + beforeEach -> + change.success = true + + it "calls remove with id and attribute name", -> + spyOn(pendingChanges, "remove").andCallFake(->) + pendingChanges.submit change + expect(pendingChanges.remove.calls.length).toEqual 1 + expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName" + + it "calls reset on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.reset).toHaveBeenCalledWith "new_value" + + it "calls success on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.success).toHaveBeenCalled() + + describe "unsuccessful request", -> + beforeEach -> + change.success = false + + it "does not call remove", -> + spyOn(pendingChanges, "remove").andCallFake(->) + pendingChanges.submit change + expect(pendingChanges.remove).not.toHaveBeenCalled() + + it "does not call reset on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.reset).not.toHaveBeenCalled() + + it "calls error on the relevant scope", -> + pendingChanges.submit change + expect(change.scope.error).toHaveBeenCalled() + + describe "cycling through all changes to submit to server", -> + it "sends the correct object to dataSubmitter", -> + spyOn(pendingChanges, "submit").andCallFake(->) + pendingChanges.pendingChanges = + 1: { "prop1": { attr: "prop1", value: 1 }, "prop2": { attr: "prop2", value: 2 } } + 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop2", value: 4 } } + 7: { "prop2": { attr: "prop2", value: 5 } } + pendingChanges.submitAll() + expect(pendingChanges.submit.calls.length).toEqual 5 + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 1 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 2 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 2 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 4 } + expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 5 } + + it "returns an array of promises representing all sumbit requests", -> + spyOn(pendingChanges, "submit").andCallFake (change) -> change.value + pendingChanges.pendingChanges = + 1: { "prop1": { attr: "prop1", value: 1 } } + 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop1", value: 4 } } + expect(pendingChanges.submitAll()).toEqual [ 1, 2, 4 ] diff --git a/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee new file mode 100644 index 0000000000..e7dedb4e92 --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee @@ -0,0 +1,52 @@ +describe "switchClass service", -> + elementMock = timeoutMock = {} + removeClass = addClass = switchClassService = null + + beforeEach -> + addClass = jasmine.createSpy('addClass') + removeClass = jasmine.createSpy('removeClass') + elementMock = + addClass: addClass + removeClass: removeClass + timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout" + timeoutMock.cancel = jasmine.createSpy('timeout.cancel') + + beforeEach -> + module "ofn.admin" , ($provide) -> + $provide.value '$timeout', timeoutMock + return + + beforeEach inject (switchClass) -> + switchClassService = switchClass + + it "calls addClass on the element once", -> + switchClassService elementMock, "addClass", [], false + expect(addClass).toHaveBeenCalledWith "addClass" + expect(addClass.calls.length).toEqual 1 + + it "calls removeClass on the element for ", -> + switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false + expect(removeClass).toHaveBeenCalledWith "remClass1" + expect(removeClass).toHaveBeenCalledWith "remClass2" + expect(removeClass).toHaveBeenCalledWith "remClass3" + expect(removeClass.calls.length).toEqual 3 + + it "call cancel on element.timout only if it exists", -> + switchClassService elementMock, "", [], false + expect(timeoutMock.cancel).not.toHaveBeenCalled() + elementMock.timeout = true + switchClassService elementMock, "", [], false + expect(timeoutMock.cancel).toHaveBeenCalled() + + it "doesn't set up a new timeout if 'timeout' is false", -> + switchClassService elementMock, "class1", ["class2"], false + expect(timeoutMock).not.toHaveBeenCalled() + + it "doesn't set up a new timeout if 'timeout' is a string", -> + switchClassService elementMock, "class1", ["class2"], "string" + expect(timeoutMock).not.toHaveBeenCalled() + + it "sets up a new timeout if 'timeout' parameter is an integer", -> + switchClassService elementMock, "class1", ["class2"], 1000 + expect(timeoutMock).toHaveBeenCalled() + expect(elementMock.timeout).toEqual "new timeout" diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 122d17f539..7c1fe5da23 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -376,236 +376,6 @@ describe "AdminOrderMgmtCtrl", -> sp = scope.filteredLineItems[0].price expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp - -describe "managing pending changes", -> - dataSubmitter = pendingChangesService = null - - beforeEach -> - dataSubmitter = jasmine.createSpy('dataSubmitter').andReturn { - then: (thenFn) -> - thenFn({propertyName: "new_value"}) - } - - beforeEach -> - module "ofn.admin", ($provide) -> - $provide.value 'dataSubmitter', dataSubmitter - return - - beforeEach inject (pendingChanges) -> - pendingChangesService = pendingChanges - - describe "adding a new change", -> - it "adds a new object with key of id if it does not already exist", -> - expect(pendingChangesService.pendingChanges).toEqual {} - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - - it "adds a new object with key of the altered attribute name if it does not already exist", -> - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } - - it "replaces the existing object when adding a change to an attribute which already exists", -> - pendingChangesService.add 1, "propertyName", { a: 1 } - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { a: 1 } - pendingChangesService.add 1, "propertyName", { b: 2 } - expect(pendingChangesService.pendingChanges["1"]["propertyName"]).toEqual { b: 2 } - - it "adds an attribute to key to a line item object when one already exists", -> - pendingChangesService.add 1, "propertyName1", { a: 1 } - pendingChangesService.add 1, "propertyName2", { b: 2 } - expect(pendingChangesService.pendingChanges["1"]).toEqual { propertyName1: { a: 1}, propertyName2: { b: 2 } } - - describe "removing all existing changes", -> - it "resets pendingChanges object", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName2"]).toBeDefined() - pendingChangesService.removeAll() - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - expect(pendingChangesService.pendingChanges).toEqual {} - - describe "removing an existing change", -> - it "deletes a change if it exists", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 }, "propertyName2": { b: 2 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).toBeDefined() - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).not.toBeDefined() - - it "deletes a line item object if it is empty", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).not.toBeDefined() - - it "does nothing if key with specified attribute does not exist", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 1, "propertyName2" - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toEqual { a: 1 } - - it "does nothing if key with specified id does not exist", -> - pendingChangesService.pendingChanges = { 1: { "propertyName1": { a: 1 } } } - expect(pendingChangesService.pendingChanges["1"]["propertyName1"]).toBeDefined() - pendingChangesService.remove 2, "propertyName1" - expect(pendingChangesService.pendingChanges["1"]).toEqual { "propertyName1": { a: 1 } } - - describe "submitting an individual change to the server", -> - it "sends the correct object to dataSubmitter", -> - changeObj = { element: {} } - pendingChangesService.submit 1, "propertyName", changeObj - expect(dataSubmitter.calls.length).toEqual 1 - expect(dataSubmitter).toHaveBeenCalledWith changeObj - - it "calls remove with id and attribute name", -> - changeObj = { element: {} } - spyOn(pendingChangesService, "remove").andCallFake(->) - pendingChangesService.submit 1, "propertyName", changeObj - expect(pendingChangesService.remove.calls.length).toEqual 1 - expect(pendingChangesService.remove).toHaveBeenCalledWith 1, "propertyName" - - it "resets the dbValue attribute of the element in question", -> - element = { dbValue: 2 } - changeObj = { element: element } - pendingChangesService.submit 1, "propertyName", changeObj - expect(element.dbValue).toEqual "new_value" - - describe "cycling through all changes to submit to server", -> - it "sends the correct object to dataSubmitter", -> - spyOn(pendingChangesService, "submit").andCallFake(->) - pendingChangesService.pendingChanges = - 1: { "prop1": 1, "prop2": 2 } - 2: { "prop1": 2, "prop2": 4 } - 7: { "prop2": 5 } - pendingChangesService.submitAll() - expect(pendingChangesService.submit.calls.length).toEqual 5 - expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop1", 1 - expect(pendingChangesService.submit).toHaveBeenCalledWith '1', "prop2", 2 - expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop1", 2 - expect(pendingChangesService.submit).toHaveBeenCalledWith '2', "prop2", 4 - expect(pendingChangesService.submit).toHaveBeenCalledWith '7', "prop2", 5 - - it "returns an array of promises representing all sumbit requests", -> - spyOn(pendingChangesService, "submit").andCallFake (id,attrName,changeObj) -> - id - pendingChangesService.pendingChanges = - 1: { "prop1": 1 } - 2: { "prop1": 2, "prop2": 4 } - expect(pendingChangesService.submitAll()).toEqual [ '1','2','2' ] - -describe "dataSubmitter service", -> - qMock = httpMock = {} - switchClassSpy = resolveSpy = rejectSpy = dataSubmitterService = null - - beforeEach -> - resolveSpy = jasmine.createSpy('resolve') - rejectSpy = jasmine.createSpy('reject') - qMock.defer = -> - resolve: resolveSpy - reject: rejectSpy - promise: "promise1" - - # Can't use httpBackend because the qMock interferes with it - httpMock.put = (url) -> - success: (successFn) -> - successFn("somedata") if url == "successURL" - error: (errorFn) -> - errorFn() if url == "errorURL" - - spyOn(httpMock, "put").andCallThrough() - spyOn(qMock, "defer").andCallThrough() - - switchClassSpy = jasmine.createSpy('switchClass') - - beforeEach -> - module "ofn.admin" , ($provide) -> - $provide.value '$q', qMock - $provide.value '$http', httpMock - $provide.value 'switchClass', switchClassSpy - return - - beforeEach inject (dataSubmitter) -> - dataSubmitterService = dataSubmitter - - it "returns a promise", -> - expect(dataSubmitterService( { url: "successURL" } )).toEqual "promise1" - expect(qMock.defer).toHaveBeenCalled() - - it "sends a PUT request with the url property of changeObj", -> - dataSubmitterService { url: "successURL" } - expect(httpMock.put).toHaveBeenCalledWith "successURL" - - it "calls resolve on deferred object when request is successful", -> - element = { a: 1 } - dataSubmitterService { url: "successURL", element: element } - expect(resolveSpy.calls.length).toEqual 1 - expect(rejectSpy.calls.length).toEqual 0 - expect(resolveSpy).toHaveBeenCalledWith "somedata" - expect(switchClassSpy).toHaveBeenCalledWith element, "update-success", ["update-pending", "update-error"], 3000 - - it "calls reject on deferred object when request is erroneous", -> - element = { b: 2 } - dataSubmitterService { url: "errorURL", element: element } - expect(resolveSpy.calls.length).toEqual 0 - expect(rejectSpy.calls.length).toEqual 1 - expect(switchClassSpy).toHaveBeenCalledWith element, "update-error", ["update-pending", "update-success"], false - -describe "switchClass service", -> - elementMock = timeoutMock = {} - removeClass = addClass = switchClassService = null - - beforeEach -> - addClass = jasmine.createSpy('addClass') - removeClass = jasmine.createSpy('removeClass') - elementMock = - addClass: addClass - removeClass: removeClass - timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout" - timeoutMock.cancel = jasmine.createSpy('timeout.cancel') - - beforeEach -> - module "ofn.admin" , ($provide) -> - $provide.value '$timeout', timeoutMock - return - - beforeEach inject (switchClass) -> - switchClassService = switchClass - - it "calls addClass on the element once", -> - switchClassService elementMock, "addClass", [], false - expect(addClass).toHaveBeenCalledWith "addClass" - expect(addClass.calls.length).toEqual 1 - - it "calls removeClass on the element for ", -> - switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false - expect(removeClass).toHaveBeenCalledWith "remClass1" - expect(removeClass).toHaveBeenCalledWith "remClass2" - expect(removeClass).toHaveBeenCalledWith "remClass3" - expect(removeClass.calls.length).toEqual 3 - - it "call cancel on element.timout only if it exists", -> - switchClassService elementMock, "", [], false - expect(timeoutMock.cancel).not.toHaveBeenCalled() - elementMock.timeout = true - switchClassService elementMock, "", [], false - expect(timeoutMock.cancel).toHaveBeenCalled() - - it "doesn't set up a new timeout if 'timeout' is false", -> - switchClassService elementMock, "class1", ["class2"], false - expect(timeoutMock).not.toHaveBeenCalled() - - it "doesn't set up a new timeout if 'timeout' is a string", -> - switchClassService elementMock, "class1", ["class2"], "string" - expect(timeoutMock).not.toHaveBeenCalled() - - it "sets up a new timeout if 'timeout' parameter is an integer", -> - switchClassService elementMock, "class1", ["class2"], 1000 - expect(timeoutMock).toHaveBeenCalled() - expect(elementMock.timeout).toEqual "new timeout" - describe "Auxiliary functions", -> describe "getting a zero filled two digit number", -> it "returns the number as a string if its value is greater than or equal to 10", -> diff --git a/spec/lib/open_food_network/enterprise_injection_data_spec.rb b/spec/lib/open_food_network/enterprise_injection_data_spec.rb new file mode 100644 index 0000000000..cb94f2374a --- /dev/null +++ b/spec/lib/open_food_network/enterprise_injection_data_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +module OpenFoodNetwork + describe EnterpriseInjectionData do + describe "relatives" do + let!(:enterprise) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + let!(:producer_inactive) { create(:supplier_enterprise, confirmed_at: nil) } + let!(:er_p) { create(:enterprise_relationship, parent: producer, child: enterprise) } + let!(:er_pi) { create(:enterprise_relationship, parent: producer_inactive, child: enterprise) } + + it "only loads activated relatives" do + subject.relatives[enterprise.id][:producers].should_not include producer_inactive.id + end + end + end +end diff --git a/spec/lib/open_food_network/order_cycle_permissions_spec.rb b/spec/lib/open_food_network/order_cycle_permissions_spec.rb index d135a3500a..275d91d13d 100644 --- a/spec/lib/open_food_network/order_cycle_permissions_spec.rb +++ b/spec/lib/open_food_network/order_cycle_permissions_spec.rb @@ -545,6 +545,16 @@ module OpenFoodNetwork expect(visible).to_not include v2 end + context "where the hub produces products" do + # NOTE: No relationship to self required + let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) } + + it "returns any variants produced by the hub" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v3 + end + end + # TODO: for backwards compatability, remove later context "when an exchange exists between the coordinator and the hub within this order cycle" do let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } @@ -712,6 +722,16 @@ module OpenFoodNetwork expect(visible).to_not include v2 end + context "where the hub produces products" do + # NOTE: No relationship to self required + let!(:v3) { create(:variant, product: create(:simple_product, supplier: hub)) } + + it "returns any variants produced by the hub" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v3 + end + end + # TODO: for backwards compatability, remove later context "when an exchange exists between the coordinator and the hub within this order cycle" do let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index 368eb4d4d7..13796c10f6 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -54,10 +54,8 @@ module OpenFoodNetwork it "fetches variants for some params" do subject.should_receive(:child_variants).and_return ["children"] - subject.should_receive(:master_variants).and_return ["masters"] subject.should_receive(:filter).with(['children']).and_return ["filter_children"] - subject.should_receive(:filter).with(['masters']).and_return ["filter_masters"] - subject.variants.should == ["filter_children", "filter_masters"] + subject.variants.should == ["filter_children"] end end @@ -92,14 +90,6 @@ module OpenFoodNetwork end end - describe "fetching master variants" do - it "doesn't return master variants with siblings" do - product = create(:simple_product, supplier: supplier) - - subject.master_variants.should be_empty - end - end - describe "Filtering variants" do let(:variants) { Spree::Variant.scoped.joins(:product).where(is_master: false) } it "should return unfiltered variants sans-params" do diff --git a/spec/lib/open_food_network/referer_parser_spec.rb b/spec/lib/open_food_network/referer_parser_spec.rb new file mode 100644 index 0000000000..13cde6099e --- /dev/null +++ b/spec/lib/open_food_network/referer_parser_spec.rb @@ -0,0 +1,23 @@ +require 'open_food_network/referer_parser' +require 'spec_helper' + +module OpenFoodNetwork + describe RefererParser do + + it "handles requests without referer" do + RefererParser.path(nil).should be_nil + end + + it "handles requests with referer" do + RefererParser.path('http://example.org/').should eq('/') + end + + it "handles requests with invalid referer" do + RefererParser.path('this is not a URI').should be_nil + end + + it "handles requests with known issue of referer" do + RefererParser.path('http://example.org/##invalid-fragment').should eq('/') + end + end +end diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb new file mode 100644 index 0000000000..8551d663a8 --- /dev/null +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -0,0 +1,37 @@ +require 'open_food_network/xero_invoices_report' + +module OpenFoodNetwork + describe XeroInvoicesReport do + subject { XeroInvoicesReport.new [] } + + describe "option defaults" do + let(:report) { XeroInvoicesReport.new [], {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + + around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } } + + it "uses defaults when blank params are passed" do + report.instance_variable_get(:@opts).should == {invoice_date: Date.civil(2015, 5, 5), + due_date: Date.civil(2015, 5, 19), + account_code: 'food sales'} + end + end + + describe "generating invoice numbers" do + let(:order) { double(:order, number: 'R731032860') } + + describe "when no initial invoice number is given" do + it "returns the order number" do + subject.send(:invoice_number_for, order, 123).should == 'R731032860' + end + end + + describe "when an initial invoice number is given" do + subject { XeroInvoicesReport.new [], {initial_invoice_number: '123'} } + + it "increments the number by the index" do + subject.send(:invoice_number_for, order, 456).should == 579 + end + end + end + end +end diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb new file mode 100644 index 0000000000..0e43ce9df5 --- /dev/null +++ b/spec/models/customer_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Customer, type: :model do + describe 'creation callbacks' do + let!(:user1) { create(:user) } + let!(:user2) { create(:user) } + let!(:enterprise) { create(:distributor_enterprise) } + + it "associates an existing user using email" do + c1 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com') + expect(c1.user).to be_nil + + c2 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com', user: user1) + expect(c2.user).to eq user1 + + c3 = Customer.create(enterprise: enterprise, email: user2.email) + expect(c3.user).to eq user2 + end + end +end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index cbbdd00504..c732d50849 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -69,4 +69,32 @@ describe EnterpriseRelationship do EnterpriseRelationship.with_permission('two').should match_array [er1, er2] end end + + describe "finding relatives" do + let(:e1) { create(:supplier_enterprise) } + let(:e2) { create(:supplier_enterprise, sells: 'any') } + let!(:er) { create(:enterprise_relationship, parent: e1, child: e2) } + let(:er_reverse) { create(:enterprise_relationship, parent: e2, child: e1) } + + it "categorises enterprises into distributors and producers" do + EnterpriseRelationship.relatives.should == + {e1.id => {distributors: Set.new([e2.id]), producers: Set.new([e2.id])}, + e2.id => {distributors: Set.new([]), producers: Set.new([e1.id])}} + end + + it "finds inactive enterprises by default" do + e1.update_attribute :confirmed_at, nil + EnterpriseRelationship.relatives[e2.id][:producers].should == Set.new([e1.id]) + end + + it "does not find inactive enterprises when requested" do + e1.update_attribute :confirmed_at, nil + EnterpriseRelationship.relatives(true)[e2.id][:producers].should be_empty + end + + it "does not show duplicates" do + er_reverse + EnterpriseRelationship.relatives[e2.id][:producers].should == Set.new([e1.id]) + end + end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index df04324af2..388d80258e 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -121,14 +121,18 @@ describe Enterprise do e.relatives.should match_array [p, c] end + it "finds relatives_including_self" do + expect(e.relatives_including_self).to include e + end + it "scopes relatives to visible distributors" do - e.should_receive(:relatives).and_return(relatives = []) + e.should_receive(:relatives_including_self).and_return(relatives = []) relatives.should_receive(:is_distributor).and_return relatives e.distributors end it "scopes relatives to visible producers" do - e.should_receive(:relatives).and_return(relatives = []) + e.should_receive(:relatives_including_self).and_return(relatives = []) relatives.should_receive(:is_primary_producer).and_return relatives e.suppliers end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 21b3d061e5..c05d3d00d5 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -422,4 +422,23 @@ describe OrderCycle do OrderCycle.first_closing_for(distributor).should == oc end end + + describe "finding the earliest closing times for each distributor" do + let(:time1) { 1.week.from_now } + let(:time2) { 2.weeks.from_now } + let(:time3) { 3.weeks.from_now } + let(:e1) { create(:distributor_enterprise) } + let(:e2) { create(:distributor_enterprise) } + let!(:oc1) { create(:simple_order_cycle, orders_close_at: time1, distributors: [e1]) } + let!(:oc2) { create(:simple_order_cycle, orders_close_at: time2, distributors: [e2]) } + let!(:oc3) { create(:simple_order_cycle, orders_close_at: time3, distributors: [e2]) } + + it "returns the closing time, indexed by enterprise id" do + OrderCycle.earliest_closing_times[e1.id].should == time1 + end + + it "returns the earliest closing time" do + OrderCycle.earliest_closing_times[e2.id].should == time2 + end + end end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 9ed432e4af..0ea8c820f1 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -220,6 +220,10 @@ module Spree should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) end + it "should not be able to access customer actions" do + should_not have_ability([:admin, :index, :update], for: Customer) + end + describe "order_cycles abilities" do context "where the enterprise is not in an order_cycle" do let!(:order_cycle) { create(:simple_order_cycle) } @@ -407,6 +411,10 @@ module Spree should_not have_ability([:sales_total, :users_and_enterprises], for: :report) end + it "should be able to access customer actions" do + should have_ability([:admin, :index, :update], for: Customer) + end + context "for a given order_cycle" do let!(:order_cycle) { create(:simple_order_cycle) } let!(:exchange){ create(:exchange, incoming: false, order_cycle: order_cycle, receiver: d1, sender: order_cycle.coordinator) } diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 579965aa7a..bd952f2e9c 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -5,6 +5,21 @@ module Spree adjustment.metadata.should be end + describe "finding adjustments with and without tax included" do + let!(:adjustment_with_tax) { create(:adjustment, included_tax: 123) } + let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) } + + it "finds adjustments with tax" do + Adjustment.with_tax.should include adjustment_with_tax + Adjustment.with_tax.should_not include adjustment_without_tax + end + + it "finds adjustments without tax" do + Adjustment.without_tax.should include adjustment_without_tax + Adjustment.without_tax.should_not include adjustment_with_tax + end + end + describe "recording included tax" do describe "TaxRate adjustments" do let!(:zone) { create(:zone_with_member) } diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index 4058ba30e1..a61f4e67fc 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -24,6 +24,22 @@ module Spree LineItem.supplied_by_any([s2]).should == [li2] LineItem.supplied_by_any([s1, s2]).should match_array [li1, li2] end + + describe "finding line items with and without tax" do + let(:tax_rate) { create(:tax_rate, calculator: Spree::Calculator::DefaultTax.new) } + let!(:adjustment1) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) } + let!(:adjustment2) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) } + + before { li1; li2 } + + it "finds line items with tax" do + LineItem.with_tax.should == [li1] + end + + it "finds line items without tax" do + LineItem.without_tax.should == [li2] + end + end end describe "calculating price with adjustments" do diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index ecc19f5493..dddaf554a2 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -511,4 +511,43 @@ describe Spree::Order do end.to enqueue_job ConfirmOrderJob end end + + describe "associating a customer" do + let(:user) { create(:user) } + let(:distributor) { create(:distributor_enterprise) } + + context "when a user has been set on the order" do + let!(:order) { create(:order, distributor: distributor, user: user) } + context "and a customer for order.distributor and order.user.email already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: user.email) } + it "associates the order with the existing customer" do + order.send(:associate_customer) + expect(order.customer).to eq customer + end + end + context "and a customer for order.distributor and order.user.email does not alread exist" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + it "creates a new customer" do + expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + end + end + end + + context "when a user has not been set on the order" do + let!(:order) { create(:order, distributor: distributor, user: nil) } + context "and a customer for order.distributor and order.email already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: order.email) } + it "creates a new customer" do + order.send(:associate_customer) + expect(order.customer).to eq customer + end + end + context "and a customer for order.distributor and order.email does not alread exist" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + it "creates a new customer" do + expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + end + end + end + end end diff --git a/spec/models/spree/shipping_method_spec.rb b/spec/models/spree/shipping_method_spec.rb index baea2ac56a..0e738470dd 100644 --- a/spec/models/spree/shipping_method_spec.rb +++ b/spec/models/spree/shipping_method_spec.rb @@ -55,5 +55,32 @@ module Spree sm.should be_available_to_order o end end + + describe "finding services offered by all distributors" do + let!(:d1) { create(:distributor_enterprise) } + let!(:d2) { create(:distributor_enterprise) } + let!(:d3) { create(:distributor_enterprise) } + let!(:d4) { create(:distributor_enterprise) } + let!(:d1_pickup) { create(:shipping_method, require_ship_address: false, distributors: [d1]) } + let!(:d1_delivery) { create(:shipping_method, require_ship_address: true, distributors: [d1]) } + let!(:d2_pickup) { create(:shipping_method, require_ship_address: false, distributors: [d2]) } + let!(:d3_delivery) { create(:shipping_method, require_ship_address: true, distributors: [d3]) } + + it "reports when the services are available" do + ShippingMethod.services[d1.id].should == {pickup: true, delivery: true} + end + + it "reports when only pickup is available" do + ShippingMethod.services[d2.id].should == {pickup: true, delivery: false} + end + + it "reports when only delivery is available" do + ShippingMethod.services[d3.id].should == {pickup: false, delivery: true} + end + + it "returns no entry when no service is available" do + ShippingMethod.services[d4.id].should be_nil + end + end end end diff --git a/spec/models/spree/taxon_spec.rb b/spec/models/spree/taxon_spec.rb new file mode 100644 index 0000000000..a0d729c054 --- /dev/null +++ b/spec/models/spree/taxon_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module Spree + describe Taxon do + let(:e) { create(:supplier_enterprise) } + let(:t0) { p1.taxons.order('id ASC').first } + let(:t1) { create(:taxon) } + let(:t2) { create(:taxon) } + + describe "finding all supplied taxons" do + let!(:p1) { create(:simple_product, supplier: e, taxons: [t1, t2]) } + + it "finds taxons" do + Taxon.supplied_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + end + end + + describe "finding all distributed taxons" do + let!(:oc) { create(:simple_order_cycle, distributors: [e], variants: [p1.master]) } + let(:s) { create(:supplier_enterprise) } + let(:p1) { create(:simple_product, supplier: s, taxons: [t1, t2]) } + + it "finds taxons" do + Taxon.distributed_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + end + end + end +end diff --git a/spec/performance/injection_helper_spec.rb b/spec/performance/injection_helper_spec.rb new file mode 100644 index 0000000000..ef8d937ce1 --- /dev/null +++ b/spec/performance/injection_helper_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe InjectionHelper, type: :helper, performance: true do + let(:oc) { create(:simple_order_cycle) } + let(:relative_supplier) { create(:supplier_enterprise) } + let(:relative_distributor) { create(:distributor_enterprise) } + + before do + 50.times do + e = create(:enterprise) + oc.distributors << e + create(:enterprise_relationship, parent: e, child: relative_supplier) + create(:enterprise_relationship, parent: e, child: relative_distributor) + end + end + + it "is performant in injecting enterprises" do + results = [] + 4.times do |i| + ActiveRecord::Base.connection.query_cache.clear + Rails.cache.clear + result = Benchmark.measure { helper.inject_enterprises } + results << result.total if i > 0 + puts result + end + + puts (results.sum / results.count * 1000).round 0 + end +end diff --git a/spec/performance/shop_controller_spec.rb b/spec/performance/shop_controller_spec.rb new file mode 100644 index 0000000000..984581a2ab --- /dev/null +++ b/spec/performance/shop_controller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe ShopController, type: :controller, performance: true do + let(:d) { create(:distributor_enterprise) } + let(:enterprise_fee) { create(:enterprise_fee) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator_fees: [enterprise_fee]) } + + before do + controller.stub(:current_distributor) { d } + controller.stub(:current_order_cycle) { order_cycle } + end + + describe "fetching products" do + let(:exchange) { order_cycle.exchanges.to_enterprises(d).outgoing.first } + let(:image) { File.open(File.expand_path('../../../app/assets/images/logo.jpg', __FILE__)) } + + before do + 11.times do + p = create(:simple_product) + p.set_property 'Organic Certified', 'NASAA 12345' + v1 = create(:variant, product: p) + v2 = create(:variant, product: p) + Spree::Image.create! viewable_id: p.master.id, viewable_type: 'Spree::Variant', attachment: image + + exchange.variants << [v1, v2] + end + end + + it "returns products via json" do + results = [] + 4.times do |i| + ActiveRecord::Base.connection.query_cache.clear + Rails.cache.clear + result = Benchmark.measure do + xhr :get, :products + response.should be_success + end + + results << result.total if i > 0 + puts result + end + + puts (results.sum / results.count * 1000).round 0 + end + end +end diff --git a/spec/requests/large_request_spec.rb b/spec/requests/large_request_spec.rb new file mode 100644 index 0000000000..6e389d5ecb --- /dev/null +++ b/spec/requests/large_request_spec.rb @@ -0,0 +1,13 @@ +# Large requests can fail if Devise tries to store the URL in the session cookie. +# +# http://daniel.fone.net.nz/blog/2014/11/28/actiondispatch-cookies-cookieoverflow-via-devise-s-user_return_to/ +require 'spec_helper' + +RSpec.describe 'A very large request', type: :request do + it 'should not overflow cookies' do + get '/admin', foo: 'x' * ActionDispatch::Cookies::SignedCookieJar::MAX_COOKIE_SIZE + expect(response.status).to eq(302) # HTTP status 302 - Found + ## Use the newer syntax if rspec gets upgraded + # expect(response).to have_http_status(:redirect) + end +end diff --git a/spec/serializers/enterprise_serializer_spec.rb b/spec/serializers/enterprise_serializer_spec.rb index 1063a042e7..f70852d973 100644 --- a/spec/serializers/enterprise_serializer_spec.rb +++ b/spec/serializers/enterprise_serializer_spec.rb @@ -1,21 +1,31 @@ #require 'spec_helper' describe Api::EnterpriseSerializer do + let(:serializer) { Api::EnterpriseSerializer.new enterprise, data: data } let(:enterprise) { create(:distributor_enterprise) } let(:taxon) { create(:taxon) } + let(:data) { OpenStruct.new(earliest_closing_times: {}, + active_distributors: [], + distributed_taxons: {enterprise.id => [123]}, + supplied_taxons: {enterprise.id => [456]}, + shipping_method_services: {}, + relatives: {enterprise.id => {producers: [123], distributors: [456]}}) } + it "serializes an enterprise" do - serializer = Api::EnterpriseSerializer.new enterprise serializer.to_json.should match enterprise.name end - it "includes distributed taxons" do - enterprise.stub(:distributed_taxons).and_return [taxon] - serializer = Api::EnterpriseSerializer.new enterprise - serializer.to_json.should match taxon.id.to_s + it "serializes taxons as ids only" do + serializer.serializable_hash[:taxons].should == [{id: 123}] + serializer.serializable_hash[:supplied_taxons].should == [{id: 456}] end - it "will render urls" do - serializer = Api::EnterpriseSerializer.new enterprise + it "serializes producers and hubs as ids only" do + serializer.serializable_hash[:producers].should == [{id: 123}] + serializer.serializable_hash[:hubs].should == [{id: 456}] + end + + it "serializes icons" do serializer.to_json.should match "map_005-hub.svg" end end diff --git a/spec/serializers/product_serializer_spec.rb b/spec/serializers/product_serializer_spec.rb deleted file mode 100644 index 0091668dbb..0000000000 --- a/spec/serializers/product_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Api::ProductSerializer do - let(:hub) { create(:distributor_enterprise) } - let(:oc) { create(:simple_order_cycle, distributors: [hub], variants: [v1]) } - let(:p) { create(:simple_product) } - let!(:v1) { create(:variant, product: p, unit_value: 3) } - let!(:v2) { create(:variant, product: p, unit_value: 5) } - - it "scopes variants to distribution" do - s = Api::ProductSerializer.new p, current_distributor: hub, current_order_cycle: oc - json = s.to_json - json.should include v1.options_text - json.should_not include v2.options_text - end -end diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb index 67cfcd81d0..cf67c7c7cb 100644 --- a/spec/support/matchers/select2_matchers.rb +++ b/spec/support/matchers/select2_matchers.rb @@ -21,6 +21,7 @@ RSpec::Matchers.define :have_select2 do |id, options={}| if results.all? results << all_options_present(from, options[:with_options]) if options.key? :with_options results << exact_options_present(from, options[:options]) if options.key? :options + results << no_options_present(from, options[:without_options]) if options.key? :without_options end results.all? @@ -51,6 +52,14 @@ RSpec::Matchers.define :have_select2 do |id, options={}| end end + def no_options_present(from, options) + with_select2_open(from) do + options.none? do |option| + @node.has_selector? "div.select2-drop-active ul.select2-results li", text: option + end + end + end + def selected_option_is(from, text) within find(from) do find("a.select2-choice").text == text diff --git a/spec/support/matchers/table_matchers.rb b/spec/support/matchers/table_matchers.rb index 053562b9e4..411b0b646f 100644 --- a/spec/support/matchers/table_matchers.rb +++ b/spec/support/matchers/table_matchers.rb @@ -26,3 +26,44 @@ RSpec::Matchers.define :have_table_row do |row| node.all('tr').map { |tr| tr.all('th, td').map(&:text) } end end + + + +# find("#my-table").should match_table [[...]] +RSpec::Matchers.define :match_table do |expected_table| + + match_for_should do |node| + rows = node. + all("tr"). + map { |r| r.all("th,td").map { |c| c.text.strip } } + + if rows.count != expected_table.count + @failure_message = "found table with #{rows.count} rows, expected #{expected_table.count}" + + else + rows.each_with_index do |row, i| + expected_row = expected_table[i] + if row.count != expected_row.count + @failure_message = "row #{i} has #{row.count} columns, expected #{expected_row.count}" + break + + elsif row != expected_row + row.each_with_index do |cell, j| + if cell != expected_row[j] + @failure_message = "cell [#{i}, #{j}] has content '#{cell}', expected '#{expected_row[j]}'" + break + end + end + break if @failure_message + end + end + end + + @failure_message.nil? + end + + failure_message_for_should do |text| + @failure_message + end + +end