diff --git a/Gemfile b/Gemfile index dec618a871..e1c4d656b8 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,9 @@ gem 'gmaps4rails' gem 'spinjs-rails' gem 'rack-ssl', :require => 'rack/ssl' +gem 'foreigner' +gem 'immigrant' + # Gems used only for assets and not required # in production environments by default. group :assets do @@ -50,7 +53,8 @@ group :assets do gem 'turbo-sprockets-rails3' gem 'foundation-icons-sass-rails' - + gem 'momentjs-rails' + gem 'angular-rails-templates' end gem "foundation-rails" gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', branch: "rails3" diff --git a/Gemfile.lock b/Gemfile.lock index 971e536da5..1b762b786c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,6 +162,9 @@ GEM acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) + angular-rails-templates (0.0.7) + railties (>= 3.1) + sprockets angularjs-rails (1.2.13) ansi (1.4.2) arel (3.0.3) @@ -261,6 +264,8 @@ GEM net-ssh (>= 2.1.3) nokogiri (~> 1.5) ruby-hmac + foreigner (1.6.1) + activerecord (>= 3.0.0) formatador (0.2.4) foundation-icons-sass-rails (3.0.0) railties (>= 3.1.1) @@ -298,6 +303,9 @@ GEM multi_json (~> 1.0) multi_xml (>= 0.5.2) i18n (0.6.9) + immigrant (0.1.6) + activerecord (>= 3.0) + foreigner (>= 1.2.1) journey (1.0.4) jquery-rails (2.2.2) railties (>= 3.0, < 5.0) @@ -327,6 +335,8 @@ GEM method_source (0.8.1) mime-types (1.25.1) mini_portile (0.5.2) + momentjs-rails (2.5.1) + railties (>= 3.1) money (5.0.0) i18n (~> 0.4) json @@ -496,6 +506,7 @@ PLATFORMS DEPENDENCIES andand + angular-rails-templates angularjs-rails awesome_print aws-sdk @@ -512,6 +523,7 @@ DEPENDENCIES eaterprises_feature! factory_girl_rails faker + foreigner foundation-icons-sass-rails foundation-rails foundation_rails_helper! @@ -523,10 +535,12 @@ DEPENDENCIES guard-rspec guard-zeus haml + immigrant jquery-rails json_spec letter_opener local_organics_feature! + momentjs-rails newrelic_rpm oj paperclip diff --git a/app/assets/images/home/groups-bg.svg b/app/assets/images/home/groups-bg.svg new file mode 100644 index 0000000000..00d2e67f23 --- /dev/null +++ b/app/assets/images/home/groups-bg.svg @@ -0,0 +1,1408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/home/maps-bg.svg b/app/assets/images/home/maps-bg.svg new file mode 100644 index 0000000000..7c486739b7 --- /dev/null +++ b/app/assets/images/home/maps-bg.svg @@ -0,0 +1,1597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/home/producers-bg.svg b/app/assets/images/home/producers-bg.svg new file mode 100644 index 0000000000..2a47eed65a --- /dev/null +++ b/app/assets/images/home/producers-bg.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/home/shopping-bg.jpg b/app/assets/images/home/shopping-bg.jpg new file mode 100644 index 0000000000..0a02b0f6f4 Binary files /dev/null and b/app/assets/images/home/shopping-bg.jpg differ diff --git a/app/assets/images/home/tagline-bg.jpg b/app/assets/images/home/tagline-bg.jpg new file mode 100644 index 0000000000..161764ef21 Binary files /dev/null and b/app/assets/images/home/tagline-bg.jpg differ diff --git a/app/assets/images/ofn_logo_beta.png b/app/assets/images/ofn_logo_beta.png new file mode 100644 index 0000000000..420a51768d Binary files /dev/null and b/app/assets/images/ofn_logo_beta.png differ diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 1056f72681..c151f4bc50 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -80,28 +80,27 @@ orderManagementModule.factory "pendingChanges",[ pendingChanges: {} add: (id, attrName, changeObj) -> - this.pendingChanges["#{id}"] = {} unless this.pendingChanges.hasOwnProperty("#{id}") - this.pendingChanges["#{id}"]["#{attrName}"] = changeObj + @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") + @pendingChanges["#{id}"]["#{attrName}"] = changeObj removeAll: -> - this.pendingChanges = {} + @pendingChanges = {} remove: (id, attrName) -> - if this.pendingChanges.hasOwnProperty("#{id}") - delete this.pendingChanges["#{id}"]["#{attrName}"] - delete this.pendingChanges["#{id}"] if this.changeCount( this.pendingChanges["#{id}"] ) < 1 + if @pendingChanges.hasOwnProperty("#{id}") + delete @pendingChanges["#{id}"]["#{attrName}"] + delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 submitAll: -> all = [] - for id,lineItem of this.pendingChanges + for id,lineItem of @pendingChanges for attrName,changeObj of lineItem - all.push this.submit(id, attrName, changeObj) + all.push @submit(id, attrName, changeObj) all submit: (id, attrName, change) -> - factory = this - dataSubmitter(change).then (data) -> - factory.remove id, attrName + dataSubmitter(change).then (data) => + @remove id, attrName change.element.dbValue = data["#{attrName}"] changeCount: (lineItem) -> @@ -151,13 +150,13 @@ orderManagementModule.controller "AdminOrderMgmtCtrl", [ $scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised" if $scope.spree_api_key_ok $http.defaults.headers.common["X-Spree-Token"] = spree_api_key - dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> + dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> $scope.suppliers = data $scope.suppliers.unshift blankOption() - dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_distributor_eq]=true").then (data) -> + dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_distributor_eq]=true").then (data) -> $scope.distributors = data $scope.distributors.unshift blankOption() - ocFetcher = dataFetcher("/api/order_cycles/managed").then (data) -> + ocFetcher = dataFetcher("/api/order_cycles/accessible").then (data) -> $scope.orderCycles = data $scope.orderCycles.unshift blankOption() $scope.fetchOrders() @@ -170,7 +169,7 @@ orderManagementModule.controller "AdminOrderMgmtCtrl", [ $scope.fetchOrders = -> $scope.loading = true - dataFetcher("/api/orders/managed?template=bulk_index&q[completed_at_not_null]=true&q[completed_at_gt]=#{$scope.startDate}&q[completed_at_lt]=#{$scope.endDate}").then (data) -> + dataFetcher("/api/orders/managed?template=bulk_index;page=1;per_page=500;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) -> $scope.resetOrders data $scope.loading = false @@ -358,4 +357,4 @@ formatTime = (date) -> twoDigitNumber = (number) -> twoDigits = "" + number twoDigits = ("0" + number) if number < 10 - twoDigits \ No newline at end of file + twoDigits diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index c7db0f1208..20aee15b74 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -328,7 +328,7 @@ productEditModule.controller "AdminProductEditCtrl", [ if confirm("Are you sure?") $http( method: "DELETE" - url: "/api/products/" + product.id + "/variants/" + variant.id + url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete" ).success (data) -> $scope.removeVariant(product, variant) @@ -635,4 +635,4 @@ subset = (bigArray,smallArray) -> return false if angular.toJson(bigArray).indexOf(angular.toJson(item)) == -1 return true else - return false \ No newline at end of file + return false diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 5b018e6548..aadf66af60 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -419,4 +419,4 @@ angular.module('order_cycle', ['ngResource']) if !$(this).is(':checked') scope.$apply -> scope.removeDistributionOfVariant(attrs.ofnSyncDistributions) - ) \ No newline at end of file + ) diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 7f09206ea3..cc9a1bbc56 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -7,11 +7,20 @@ #= require angular-cookies #= require angular-resource #= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT +#= require ../shared/bindonce.min.js +#= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js +#= require_tree ../../templates +#= require angular-backstretch.js +#= require angular-flash.min.js +#= require moment +#= require modernizr # -#= require ../shared/jquery.timeago #= require foundation #= require ./darkswarm +#= require ./overrides +#= require_tree ./mixins +#= require_tree ./directives #= require_tree . $ -> @@ -19,4 +28,3 @@ $ -> Foundation.set_namespace = -> null $(document).foundation() - $(document).foundation({reveal: {animation: 'fade'}}) diff --git a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee index b7180fab49..4a31ce5697 100644 --- a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee @@ -1,4 +1,4 @@ -window.AccountSidebarCtrl = Darkswarm.controller "AccountSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> +Darkswarm.controller "AccountSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> $scope.path = "/account" Navigation.paths.push $scope.path @@ -9,7 +9,6 @@ window.AccountSidebarCtrl = Darkswarm.controller "AccountSidebarCtrl", ($scope, Navigation.navigate($scope.path) $scope.emptyCart = (href, ev)-> - console.log href if $(ev.delegateTarget).hasClass "empties-cart" location.href = href if confirm "Changing your Hub will clear your cart." else diff --git a/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee index 188867d7da..e8716e750b 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication_actions_controller.js.coffee @@ -1,12 +1,5 @@ -window.AuthenticationActionsCtrl = Darkswarm.controller "AuthenticationActionsCtrl", ($scope, Navigation, storage) -> - $scope.toggleLogin = -> - Navigation.navigate "/login" +Darkswarm.controller "AuthenticationActionsCtrl", ($scope, Navigation, storage, Sidebar) -> + $scope.Sidebar = Sidebar - $scope.toggleSignup = -> - Navigation.navigate "/signup" - - $scope.toggleSignup = -> - Navigation.navigate "/signup" - - $scope.toggle = (path = null)-> + $scope.toggle = (path)-> Navigation.navigate(path) diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee new file mode 100644 index 0000000000..33b9ed9184 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/billing_controller.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.controller "BillingCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "billing" + $scope.nextPanel = "shipping" + + diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee new file mode 100644 index 0000000000..dbf273e9e8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.controller "DetailsCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "details" + $scope.nextPanel = "billing" + + + #$scope.$watch -> + #$scope.detailsValid() + #, (valid)-> + #if valid + #$scope.show("billing") + diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee new file mode 100644 index 0000000000..ee5ff84b4a --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "PaymentCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "payment" diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee new file mode 100644 index 0000000000..a6a38329b4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/shipping_controller.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.controller "ShippingCtrl", ($scope) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.name = "shipping" + $scope.nextPanel = "payment" + diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee index d07e023656..a342864dd5 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,36 +1,38 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, $rootScope, order, $location, $anchorScroll) -> - $scope.require_ship_address = false - $scope.order = order - $scope.userOpen = true +Darkswarm.controller "CheckoutCtrl", ($scope, Order, storage, CheckoutFormState, User) -> - $scope.initialize = -> - # Our shipping_methods comes through as a hash like so: {id: requires_shipping_address} - # Here we default to the first shipping method if none is selected - $scope.order.shipping_method_id ||= Object.keys(order.shipping_methods)[0] - $scope.order.ship_address_same_as_billing = true if $scope.order.ship_address_same_as_billing == null - $scope.shippingMethodChanged() - - $scope.shippingPrice = -> - $scope.shippingMethod().price + # We put Order.order into the scope for convenience + # However, storage.bind replaces Order.order + # So we must put Order.order into the scope AFTER it's bound to localStorage + $scope.Order = Order + storage.bind $scope, "Order.order", {storeName: "order_#{Order.order.id}"} + $scope.order = Order.order - $scope.cartTotal = -> - $scope.shippingPrice() + $scope.order.display_total + if User + $scope.accordion = {details: true} + else + $scope.accordion = {user: true} + $scope.show = (name)-> + $scope.accordion[name] = true + storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}"} + # If we are logged in, but the cached accordion panel is user, move to details + if User and $scope.accordion.user + $scope.accordion.user = false + $scope.accordion.details = true - $scope.shippingMethod = -> - $scope.order.shipping_methods[$scope.order.shipping_method_id] + # TODO MAKE THIS BETTER SOMEHOW + # if User + # show details + # else + # show user + # + # localStorage overrides above + # + # If localStorage set to user, but User exists + # Then default to details - $scope.shippingMethodChanged = -> - $scope.require_ship_address = $scope.shippingMethod().require_ship_address if $scope.shippingMethod() + $scope.CheckoutFormState = CheckoutFormState + storage.bind $scope, "CheckoutFormState.ship_address_same_as_billing", { defaultValue: true} $scope.purchase = (event)-> event.preventDefault() - checkout.submit() - - $scope.scrollTo = (name)-> - #$scope.userOpen = false - $("#order_email").focus() - $location.hash(name); - $anchorScroll(); - - $scope.initialize() - + $scope.Order.submit() diff --git a/app/assets/javascripts/darkswarm/controllers/current_hub_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/current_hub_controller.js.coffee new file mode 100644 index 0000000000..80a2af33fe --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/current_hub_controller.js.coffee @@ -0,0 +1,2 @@ +Darkswarm.controller "CurrentHubCtrl", ($scope, CurrentHub) -> + $scope.CurrentHub = CurrentHub diff --git a/app/assets/javascripts/darkswarm/controllers/forgot_sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/forgot_sidebar_controller.js.coffee index e3c272a3af..4300461a0d 100644 --- a/app/assets/javascripts/darkswarm/controllers/forgot_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/forgot_sidebar_controller.js.coffee @@ -1,4 +1,4 @@ -window.ForgotSidebarCtrl = Darkswarm.controller "ForgotSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> +Darkswarm.controller "ForgotSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> $scope.spree_user = SpreeUser.spree_user $scope.path = "/forgot" $scope.sent = false diff --git a/app/assets/javascripts/darkswarm/controllers/hub_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/hub_node_controller.js.coffee new file mode 100644 index 0000000000..f17d985434 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/hub_node_controller.js.coffee @@ -0,0 +1,20 @@ +Darkswarm.controller "HubNodeCtrl", ($scope, Navigation, $location, $anchorScroll, $templateCache, CurrentHub) -> + $scope.toggle = -> + Navigation.navigate $scope.hub.path + + $scope.open = -> + $location.path() == $scope.hub.path + + $scope.current = -> + $scope.hub.id is CurrentHub.id + + $scope.emptiesCart = -> + CurrentHub.id isnt undefined and !$scope.current() + + $scope.changeHub = -> + if confirm "Are you sure? This will change your selected Hub and remove any items in you shopping cart." + Navigation.go $scope.hub.path + + if $scope.open() + $anchorScroll() + diff --git a/app/assets/javascripts/darkswarm/controllers/hubs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/hubs_controller.js.coffee new file mode 100644 index 0000000000..0be4166dcb --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/hubs_controller.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.controller "HubsCtrl", ($scope, Hubs) -> + $scope.Hubs = Hubs + $scope.hubs = Hubs.hubs diff --git a/app/assets/javascripts/darkswarm/controllers/login_sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/login_sidebar_controller.js.coffee index 54b10b0b4a..63d31a8f3c 100644 --- a/app/assets/javascripts/darkswarm/controllers/login_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/login_sidebar_controller.js.coffee @@ -1,4 +1,4 @@ -window.LoginSidebarCtrl = Darkswarm.controller "LoginSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> +Darkswarm.controller "LoginSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> $scope.spree_user = SpreeUser.spree_user $scope.path = "/login" Navigation.paths.push $scope.path diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee index 34ec81c5d2..9ba11baa5b 100644 --- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee @@ -1,8 +1,13 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) -> $scope.data = Product.data + $scope.limit = 3 $scope.order_cycle = OrderCycle.order_cycle Product.update() + $scope.incrementLimit = -> + if $scope.limit < $scope.data.products.length + $scope.limit = $scope.limit + 1 + $scope.searchKeypress = (e)-> code = e.keyCode || e.which if code == 13 diff --git a/app/assets/javascripts/darkswarm/controllers/sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/sidebar_controller.js.coffee index 8544f57922..da18067f36 100644 --- a/app/assets/javascripts/darkswarm/controllers/sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/sidebar_controller.js.coffee @@ -1,5 +1,2 @@ -window.SidebarCtrl = Darkswarm.controller "SidebarCtrl", ($scope, $location) -> - $scope.sidebarPaths = ["/login", "/signup", "/forgot", "/account"] - - $scope.active = -> - $location.path() in $scope.sidebarPaths +Darkswarm.controller "SidebarCtrl", ($scope, $location, Sidebar) -> + $scope.Sidebar = Sidebar diff --git a/app/assets/javascripts/darkswarm/controllers/signup_sidebar_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/signup_sidebar_controller.js.coffee index 14c924e275..ef6ded77fd 100644 --- a/app/assets/javascripts/darkswarm/controllers/signup_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/signup_sidebar_controller.js.coffee @@ -1,4 +1,4 @@ -window.SignupSidebarCtrl = Darkswarm.controller "SignupSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> +Darkswarm.controller "SignupSidebarCtrl", ($scope, $http, $location, SpreeUser, Navigation) -> $scope.spree_user = SpreeUser.spree_user $scope.path = "/signup" Navigation.paths.push $scope.path diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index d2ce7b6758..9382f70d46 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,11 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage']).config ($httpProvider, $tooltipProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", + 'mm.foundation', + 'angularLocalStorage', + 'pasvaz.bindonce', + 'infinite-scroll', + 'angular-flash.service', + 'templates', + 'backstretch']).config ($httpProvider, $tooltipProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' diff --git a/app/assets/javascripts/darkswarm/directives/debounce.js.coffee b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee new file mode 100644 index 0000000000..343fcb531a --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/debounce.js.coffee @@ -0,0 +1,25 @@ +Darkswarm.directive "ngDebounce", ($timeout) -> + restrict: "A" + require: "ngModel" + priority: 99 + link: (scope, elm, attr, ngModelCtrl) -> + return if attr.type is "radio" or attr.type is "checkbox" + elm.unbind "input" + debounce = undefined + elm.bind "keydown paste", -> + $timeout.cancel debounce + debounce = $timeout(-> + scope.$apply -> + ngModelCtrl.$setViewValue elm.val() + return + return + , attr.ngDebounce or 1000) + return + + elm.bind "blur", -> + scope.$apply -> + ngModelCtrl.$setViewValue elm.val() + return + return + return + diff --git a/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee new file mode 100644 index 0000000000..352ed85fd6 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/disable_enter.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.directive "ofnDisableEnter", ()-> + restrict: 'A' + link: (scope, element, attrs)-> + element.bind "keydown keypress", (e)-> + code = e.keyCode || e.which + if code == 13 + e.preventDefault() diff --git a/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee new file mode 100644 index 0000000000..7c870ab860 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/disable_scroll.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.directive "ofnDisableScroll", ()-> + restrict: 'A' + + link: (scope, element, attrs)-> + element.bind 'focus', -> + element.bind 'mousewheel', (e)-> + e.preventDefault() + element.bind 'blur', -> + element.unbind 'mousewheel' diff --git a/app/assets/javascripts/darkswarm/directives/flash.js.coffee b/app/assets/javascripts/darkswarm/directives/flash.js.coffee new file mode 100644 index 0000000000..86eb2a05a2 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/flash.js.coffee @@ -0,0 +1,15 @@ +Darkswarm.directive "ofnFlash", (flash, $timeout)-> + scope: {} + restrict: 'AE' + template: "{{flash.message}}" + link: ($scope, element, attr) -> + $scope.flashes = [] + show = (message, type)-> + if message + $scope.flashes.push({message: message, type: type}) + $timeout($scope.delete, 5000) + + $scope.delete = -> + $scope.flashes.shift() + + flash.subscribe(show) diff --git a/app/assets/javascripts/darkswarm/directives/focus.js.coffee b/app/assets/javascripts/darkswarm/directives/focus.js.coffee new file mode 100644 index 0000000000..c481702d6c --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/focus.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.directive "ofnFocus", -> + restrict: "A" + link: (scope, element, attrs) -> + scope.$watch attrs.ofnFocus, ((focus) -> + focus and element.focus() + return + ), true + + return diff --git a/app/assets/javascripts/darkswarm/directives/mailto.js.coffee b/app/assets/javascripts/darkswarm/directives/mailto.js.coffee new file mode 100644 index 0000000000..27299005a9 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/mailto.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "mailto", (Navigation)-> + restrict: 'A' + link: (scope, element, attrs)-> + element.bind 'click', (e)-> + e.preventDefault() + window.location.href = "mailto:#{attrs.href.split("").reverse().join("")}" diff --git a/app/assets/javascripts/darkswarm/directives/modal.js.coffee b/app/assets/javascripts/darkswarm/directives/modal.js.coffee new file mode 100644 index 0000000000..8c34ec18a8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/modal.js.coffee @@ -0,0 +1,14 @@ +Darkswarm.directive "ofnModal", ($modal)-> + restrict: 'E' + replace: true + transclude: true + template: "{{title}}" + + link: (scope, elem, attrs, ctrl, transclude)-> + scope.title = attrs.title + + scope.cancel = -> + scope.modalInstance.dismiss("cancel") + + elem.on "click", -> + scope.modalInstance = $modal.open(controller: ctrl, template: transclude()) diff --git a/app/assets/javascripts/darkswarm/filters/dates.js.coffee b/app/assets/javascripts/darkswarm/filters/dates.js.coffee new file mode 100644 index 0000000000..9091c80c7c --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/dates.js.coffee @@ -0,0 +1,10 @@ +Darkswarm.filter "date_in_words", -> + (date) -> + moment(date).fromNow() + +Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)-> + (date) -> + if moment().add('days', 2) < moment(date) + "Orders open" + else + "Closing in #{date_in_wordsFilter(date)}" diff --git a/app/assets/javascripts/darkswarm/filters/filter_hubs.js.coffee b/app/assets/javascripts/darkswarm/filters/filter_hubs.js.coffee new file mode 100644 index 0000000000..5b3084caf5 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/filter_hubs.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.filter 'filterHubs', -> + (hubs, text) -> + hubs ||= [] + text ?= "" + match = (matched)-> + matched.toLowerCase().indexOf(text.toLowerCase()) != -1 + + hubs.filter (hub)-> + match(hub.name) or match(hub.address.zipcode) or match(hub.address.city) diff --git a/app/assets/javascripts/darkswarm/filters/print_array.js.coffee b/app/assets/javascripts/darkswarm/filters/print_array.js.coffee new file mode 100644 index 0000000000..344eeac144 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/print_array.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.filter "printArray", -> + (array, attr = 'name')-> + array ?= [] + array.map (a)-> + a[attr].toLowerCase() + .join(", ") diff --git a/app/assets/javascripts/darkswarm/shop.js.coffee b/app/assets/javascripts/darkswarm/filters/truncate.js.coffee similarity index 60% rename from app/assets/javascripts/darkswarm/shop.js.coffee rename to app/assets/javascripts/darkswarm/filters/truncate.js.coffee index 70917a934d..0f05cbc1b4 100644 --- a/app/assets/javascripts/darkswarm/shop.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/truncate.js.coffee @@ -1,4 +1,4 @@ -angular.module("filters", []).filter "truncate", -> +Darkswarm.filter "truncate", -> (text, length, end) -> text = text || "" length = 10 if isNaN(length) @@ -7,8 +7,3 @@ angular.module("filters", []).filter "truncate", -> text else String(text).substring(0, length - end.length) + end - -$.timeago.settings.allowFuture = true; -angular.module("filters").filter "date_in_words", -> - (date) -> - $.timeago(date) diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee new file mode 100644 index 0000000000..ff01db5483 --- /dev/null +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -0,0 +1,38 @@ +window.FieldsetMixin = ($scope)-> + $scope.next = (event)-> + event.preventDefault() + $scope.show $scope.nextPanel + + $scope.valid = -> + $scope.form().$valid + + $scope.form = -> + $scope[$scope.name] + + $scope.field = (path)-> + $scope.form()[path] + + $scope.fieldValid = (path)-> + not ($scope.dirty(path) and $scope.invalid(path)) + + $scope.dirty = (name)-> + $scope.field(name).$dirty + + $scope.invalid = (name)-> + $scope.field(name).$invalid + + $scope.error = (name)-> + $scope.field(name).$error + + $scope.fieldErrors = (path)-> + errors = for error, invalid of $scope.error(path) + if invalid + switch error + when "required" then "can't be blank" + when "number" then "must be number" + when "email" then "must be email address" + + #server_errors = $scope.Order.errors[path.replace('order.', '')] + #errors.push server_errors if server_errors? + (errors.filter (error) -> error?).join ", " + diff --git a/app/assets/javascripts/darkswarm/overrides.js.coffee b/app/assets/javascripts/darkswarm/overrides.js.coffee index 4d678c77a1..b9dd47500b 100644 --- a/app/assets/javascripts/darkswarm/overrides.js.coffee +++ b/app/assets/javascripts/darkswarm/overrides.js.coffee @@ -1,20 +1,4 @@ -#Foundation.libs.section.toggle_active = (e)-> - #$this = $(this) - #self = Foundation.libs.section - #region = $this.parent() - #content = $this.siblings(self.settings.content_selector) - #section = region.parent() - #settings = $.extend({}, self.settings, self.data_options(section)) - #prev_active_region = section.children(self.settings.region_selector).filter("." + self.settings.active_class) - - ##for anchors inside [data-section-title] - #e.preventDefault() if not settings.deep_linking and content.length > 0 - #e.stopPropagation() #do not catch same click again on parent - #unless region.hasClass(self.settings.active_class) - #prev_active_region.removeClass self.settings.active_class - #region.addClass self.settings.active_class - ##force resize for better performance (do not wait timer) - #self.resize region.find(self.settings.section_selector).not("[" + self.settings.resized_data_attr + "]"), true - #else if not settings.one_up# and (self.small(section) or self.is_vertical_nav(section) or self.is_horizontal_nav(section) or self.is_accordion(section)) - #region.removeClass self.settings.active_class - #settings.callback section +Array::unique = -> + output = {} + output[@[key]] = @[key] for key in [0...@length] + value for key, value of output diff --git a/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee new file mode 100644 index 0000000000..9d68efba64 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee @@ -0,0 +1,3 @@ +Darkswarm.factory 'CheckoutFormState', ()-> + # Just a singleton place to store data about the form statr + new class CheckoutFormState diff --git a/app/assets/javascripts/darkswarm/services/hub.js.coffee b/app/assets/javascripts/darkswarm/services/hub.js.coffee new file mode 100644 index 0000000000..68fcd6e3f6 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/hub.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.factory 'CurrentHub', ($location, $filter, currentHub) -> + new class CurrentHub + constructor: -> + @[k] = v for k, v of currentHub + diff --git a/app/assets/javascripts/darkswarm/services/hubs.js.coffee b/app/assets/javascripts/darkswarm/services/hubs.js.coffee new file mode 100644 index 0000000000..a3f2b03f23 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/hubs.js.coffee @@ -0,0 +1,4 @@ +Darkswarm.factory 'Hubs', ($location, hubs, $filter, CurrentHub) -> + new class Hubs + constructor: -> + @hubs = $filter('orderBy')(hubs, ['-active', '+orders_close_at']) diff --git a/app/assets/javascripts/darkswarm/services/navigation.js.coffee b/app/assets/javascripts/darkswarm/services/navigation.js.coffee index a89523dbff..923aa9cf0d 100644 --- a/app/assets/javascripts/darkswarm/services/navigation.js.coffee +++ b/app/assets/javascripts/darkswarm/services/navigation.js.coffee @@ -1,12 +1,14 @@ Darkswarm.factory 'Navigation', ($location) -> new class Navigation paths: [] - path: null + path: null navigate: (path = false)-> @path = path || @path || @paths[0] - if $location.path() == @path $location.path("/") else $location.path(@path) + + go: (path)-> + window.location.pathname = path diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee new file mode 100644 index 0000000000..3c86afac9f --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -0,0 +1,50 @@ +Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState, flash, Navigation)-> + new class Order + errors: {} + + constructor: -> + @order = order + # Default to first shipping method if none selected + @order.shipping_method_id ||= parseInt(Object.keys(@order.shipping_methods)[0]) + + submit: -> + $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> + Navigation.go data.path + .error (response, status)=> + @errors = response.errors + flash.error = response.flash?.error + flash.success = response.flash?.notice + + # Rails wants our Spree::Address data to be provided with _attributes + preprocess: -> + munged_order = {} + for name, value of @order # Clone all data from the order JSON object + switch name + when "bill_address" + munged_order["bill_address_attributes"] = value + when "ship_address" + munged_order["ship_address_attributes"] = value + when "payment_method_id" + munged_order["payments_attributes"] = [{payment_method_id: value}] + when "form_state" # don't keep this shit + else + munged_order[name] = value + + if CheckoutFormState.ship_address_same_as_billing + munged_order.ship_address_attributes = munged_order.bill_address_attributes + munged_order + + shippingMethod: -> + @order.shipping_methods[@order.shipping_method_id] + + requireShipAddress: -> + @shippingMethod()?.require_ship_address + + shippingPrice: -> + @shippingMethod()?.price + + paymentMethod: -> + @order.payment_methods[@order.payment_method_id] + + cartTotal: -> + @shippingPrice() + @order.display_total diff --git a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee index c7a4473b03..1bb23446a2 100644 --- a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee +++ b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee @@ -1,14 +1,13 @@ Darkswarm.factory 'OrderCycle', ($resource, Product, orderCycleData) -> class OrderCycle - @order_cycle = orderCycleData || null + @order_cycle = orderCycleData # Object or {} due to RABL @push_order_cycle: -> new $resource("/shop/order_cycle").save {order_cycle_id: @order_cycle.order_cycle_id}, (order_data)-> OrderCycle.order_cycle.orders_close_at = order_data.orders_close_at Product.update() @orders_close_at: -> - if @selected() - @order_cycle.orders_close_at + @order_cycle.orders_close_at if @selected() @selected: -> - @order_cycle != null and !$.isEmptyObject(@order_cycle) and @order_cycle.orders_close_at != undefined + !$.isEmptyObject(@order_cycle) and @order_cycle.orders_close_at? diff --git a/app/assets/javascripts/darkswarm/services/sidebar.js.coffee b/app/assets/javascripts/darkswarm/services/sidebar.js.coffee new file mode 100644 index 0000000000..c05f6659f2 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/sidebar.js.coffee @@ -0,0 +1,13 @@ +Darkswarm.factory "Sidebar", ($location, Navigation)-> + new class Sidebar + paths: ["/login", "/signup", "/forgot", "/account"] + + active: -> + $location.path() in @paths + + toggle: -> + if Navigation.path in @paths + Navigation.navigate(Navigation.path) + else + Navigation.navigate(@paths[0]) + diff --git a/app/assets/javascripts/darkswarm/services/spree_user.js.coffee b/app/assets/javascripts/darkswarm/services/spree_user.js.coffee index f57a6e183b..f16ede8a46 100644 --- a/app/assets/javascripts/darkswarm/services/spree_user.js.coffee +++ b/app/assets/javascripts/darkswarm/services/spree_user.js.coffee @@ -1,6 +1,7 @@ -Darkswarm.factory 'SpreeUser', ($resource) -> +Darkswarm.factory 'SpreeUser', () -> + # This is for storing Login/Signup/Forgot data to send to server + # This does NOT represent our current user new class SpreeUser - spree_user: { + spree_user: remember_me: 0 email: null - } diff --git a/app/assets/javascripts/darkswarm/services/user.js.coffee b/app/assets/javascripts/darkswarm/services/user.js.coffee new file mode 100644 index 0000000000..2a71d6c7b8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/user.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.factory 'User', (user)-> + # This is for the current user + if user and !$.isEmptyObject(user) + new class User + constructor: -> + @[k] = v for k, v of user + else + undefined diff --git a/app/assets/javascripts/shared/angular-mocks.js b/app/assets/javascripts/shared/angular-mocks.js deleted file mode 100644 index aad5452b89..0000000000 --- a/app/assets/javascripts/shared/angular-mocks.js +++ /dev/null @@ -1,1741 +0,0 @@ - -/** - * @license AngularJS v1.0.3 - * (c) 2010-2012 Google, Inc. http://angularjs.org - * License: MIT - * - * TODO(vojta): wrap whole file into closure during build - */ - -/** - * @ngdoc overview - * @name angular.mock - * @description - * - * Namespace from 'angular-mocks.js' which contains testing related code. - */ -angular.mock = {}; - -/** - * ! This is a private undocumented service ! - * - * @name ngMock.$browser - * - * @description - * This service is a mock implementation of {@link ng.$browser}. It provides fake - * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, - * cookies, etc... - * - * The api of this service is the same as that of the real {@link ng.$browser $browser}, except - * that there are several helper methods available which can be used in tests. - */ -angular.mock.$BrowserProvider = function() { - this.$get = function(){ - return new angular.mock.$Browser(); - }; -}; - -angular.mock.$Browser = function() { - var self = this; - - this.isMock = true; - self.$$url = "http://server/"; - self.$$lastUrl = self.$$url; // used by url polling fn - self.pollFns = []; - - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = angular.noop; - self.$$incOutstandingRequestCount = angular.noop; - - - // register url polling fn - - self.onUrlChange = function(listener) { - self.pollFns.push( - function() { - if (self.$$lastUrl != self.$$url) { - self.$$lastUrl = self.$$url; - listener(self.$$url); - } - } - ); - - return listener; - }; - - self.cookieHash = {}; - self.lastCookieHash = {}; - self.deferredFns = []; - self.deferredNextId = 0; - - self.defer = function(fn, delay) { - delay = delay || 0; - self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a,b){ return a.time - b.time;}); - return self.deferredNextId++; - }; - - - self.defer.now = 0; - - - self.defer.cancel = function(deferId) { - var fnIndex; - - angular.forEach(self.deferredFns, function(fn, index) { - if (fn.id === deferId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - self.deferredFns.splice(fnIndex, 1); - return true; - } - - return false; - }; - - - /** - * @name ngMock.$browser#defer.flush - * @methodOf ngMock.$browser - * - * @description - * Flushes all pending requests and executes the defer callbacks. - * - * @param {number=} number of milliseconds to flush. See {@link #defer.now} - */ - self.defer.flush = function(delay) { - if (angular.isDefined(delay)) { - self.defer.now += delay; - } else { - if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length-1].time; - } else { - throw Error('No deferred tasks to be flushed'); - } - } - - while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { - self.deferredFns.shift().fn(); - } - }; - /** - * @name ngMock.$browser#defer.now - * @propertyOf ngMock.$browser - * - * @description - * Current milliseconds mock time. - */ - - self.$$baseHref = ''; - self.baseHref = function() { - return this.$$baseHref; - }; -}; -angular.mock.$Browser.prototype = { - -/** - * @name ngMock.$browser#poll - * @methodOf ngMock.$browser - * - * @description - * run all fns in pollFns - */ - poll: function poll() { - angular.forEach(this.pollFns, function(pollFn){ - pollFn(); - }); - }, - - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - - url: function(url, replace) { - if (url) { - this.$$url = url; - return this; - } - - return this.$$url; - }, - - cookies: function(name, value) { - if (name) { - if (value == undefined) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } - }, - - notifyWhenNoOutstandingRequests: function(fn) { - fn(); - } -}; - - -/** - * @ngdoc object - * @name ngMock.$exceptionHandlerProvider - * - * @description - * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed - * into the `$exceptionHandler`. - */ - -/** - * @ngdoc object - * @name ngMock.$exceptionHandler - * - * @description - * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration - * information. - */ - -angular.mock.$ExceptionHandlerProvider = function() { - var handler; - - /** - * @ngdoc method - * @name ngMock.$exceptionHandlerProvider#mode - * @methodOf ngMock.$exceptionHandlerProvider - * - * @description - * Sets the logging mode. - * - * @param {string} mode Mode of operation, defaults to `rethrow`. - * - * - `rethrow`: If any errors are are passed into the handler in tests, it typically - * means that there is a bug in the application or test, so this mock will - * make these tests fail. - * - `log`: Sometimes it is desirable to test that an error is throw, for this case the `log` mode stores the - * error and allows later assertion of it. - * See {@link ngMock.$log#assertEmpty assertEmpty()} and - * {@link ngMock.$log#reset reset()} - */ - this.mode = function(mode) { - switch(mode) { - case 'rethrow': - handler = function(e) { - throw e; - }; - break; - case 'log': - var errors = []; - - handler = function(e) { - if (arguments.length == 1) { - errors.push(e); - } else { - errors.push([].slice.call(arguments, 0)); - } - }; - - handler.errors = errors; - break; - default: - throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); - } - }; - - this.$get = function() { - return handler; - }; - - this.mode('rethrow'); -}; - - -/** - * @ngdoc service - * @name ngMock.$log - * - * @description - * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays - * (one array per logging level). These arrays are exposed as `logs` property of each of the - * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. - * - */ -angular.mock.$LogProvider = function() { - - function concat(array1, array2, index) { - return array1.concat(Array.prototype.slice.call(array2, index)); - } - - - this.$get = function () { - var $log = { - log: function() { $log.log.logs.push(concat([], arguments, 0)); }, - warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, - info: function() { $log.info.logs.push(concat([], arguments, 0)); }, - error: function() { $log.error.logs.push(concat([], arguments, 0)); } - }; - - /** - * @ngdoc method - * @name ngMock.$log#reset - * @methodOf ngMock.$log - * - * @description - * Reset all of the logging arrays to empty. - */ - $log.reset = function () { - /** - * @ngdoc property - * @name ngMock.$log#log.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.log.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#warn.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.warn.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#info.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.info.logs = []; - /** - * @ngdoc property - * @name ngMock.$log#error.logs - * @propertyOf ngMock.$log - * - * @description - * Array of logged messages. - */ - $log.error.logs = []; - }; - - /** - * @ngdoc method - * @name ngMock.$log#assertEmpty - * @methodOf ngMock.$log - * - * @description - * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. - */ - $log.assertEmpty = function() { - var errors = []; - angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { - angular.forEach($log[logLevel].logs, function(log) { - angular.forEach(log, function (logItem) { - errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); - }); - }); - }); - if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + - "log message was not checked and removed:"); - errors.push(''); - throw new Error(errors.join('\n---------\n')); - } - }; - - $log.reset(); - return $log; - }; -}; - - -(function() { - var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; - - function jsonStringToDate(string){ - var match; - if (match = string.match(R_ISO8061_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0; - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); - } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); - return date; - } - return string; - } - - function int(str) { - return parseInt(str, 10); - } - - function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; - } - - - /** - * @ngdoc object - * @name angular.mock.TzDate - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. - * - * Mock of the Date type which has its timezone specified via constroctor arg. - * - * The main purpose is to create Date-like instances with timezone fixed to the specified timezone - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - * - *
-   * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
-   * newYearInBratislava.getTimezoneOffset() => -60;
-   * newYearInBratislava.getFullYear() => 2010;
-   * newYearInBratislava.getMonth() => 0;
-   * newYearInBratislava.getDate() => 1;
-   * newYearInBratislava.getHours() => 0;
-   * newYearInBratislava.getMinutes() => 0;
-   * 
- * - */ - angular.mock.TzDate = function (offset, timestamp) { - var self = new Date(0); - if (angular.isString(timestamp)) { - var tsStr = timestamp; - - self.origDate = jsonStringToDate(timestamp); - - timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) - throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" - }; - } else { - self.origDate = new Date(timestamp); - } - - var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; - self.date = new Date(timestamp + self.offsetDiff); - - self.getTime = function() { - return self.date.getTime() - self.offsetDiff; - }; - - self.toLocaleDateString = function() { - return self.date.toLocaleDateString(); - }; - - self.getFullYear = function() { - return self.date.getFullYear(); - }; - - self.getMonth = function() { - return self.date.getMonth(); - }; - - self.getDate = function() { - return self.date.getDate(); - }; - - self.getHours = function() { - return self.date.getHours(); - }; - - self.getMinutes = function() { - return self.date.getMinutes(); - }; - - self.getSeconds = function() { - return self.date.getSeconds(); - }; - - self.getTimezoneOffset = function() { - return offset * 60; - }; - - self.getUTCFullYear = function() { - return self.origDate.getUTCFullYear(); - }; - - self.getUTCMonth = function() { - return self.origDate.getUTCMonth(); - }; - - self.getUTCDate = function() { - return self.origDate.getUTCDate(); - }; - - self.getUTCHours = function() { - return self.origDate.getUTCHours(); - }; - - self.getUTCMinutes = function() { - return self.origDate.getUTCMinutes(); - }; - - self.getUTCSeconds = function() { - return self.origDate.getUTCSeconds(); - }; - - self.getUTCMilliseconds = function() { - return self.origDate.getUTCMilliseconds(); - }; - - self.getDay = function() { - return self.date.getDay(); - }; - - // provide this method only on browsers that already have it - if (self.toISOString) { - self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' - } - } - - //hide all methods not implemented in this mock that the Date prototype exposes - var unimplementedMethods = ['getMilliseconds', 'getUTCDay', - 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', - 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', - 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', - 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', - 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - - angular.forEach(unimplementedMethods, function(methodName) { - self[methodName] = function() { - throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); - }; - }); - - return self; - }; - - //make "tzDateInstance instanceof Date" return true - angular.mock.TzDate.prototype = Date.prototype; -})(); - - -/** - * @ngdoc function - * @name angular.mock.debug - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available function. - * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. - * - * This method is also available on window, where it can be used to display objects on debug console. - * - * @param {*} object - any object to turn into string. - * @return {string} a serialized string of the argument - */ -angular.mock.dump = function(object) { - return serialize(object); - - function serialize(object) { - var out; - - if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
'); - angular.forEach(object, function(element) { - out.append(angular.element(element).clone()); - }); - out = out.html(); - } else if (angular.isArray(object)) { - out = []; - angular.forEach(object, function(o) { - out.push(serialize(o)); - }); - out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { - if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { - out = serializeScope(object); - } else if (object instanceof Error) { - out = object.stack || ('' + object.name + ': ' + object.message); - } else { - out = angular.toJson(object, true); - } - } else { - out = String(object); - } - - return out; - } - - function serializeScope(scope, offset) { - offset = offset || ' '; - var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { - if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { - log.push(' ' + key + ': ' + angular.toJson(scope[key])); - } - } - var child = scope.$$childHead; - while(child) { - log.push(serializeScope(child, offset + ' ')); - child = child.$$nextSibling; - } - log.push('}'); - return log.join('\n' + offset); - } -}; - -/** - * @ngdoc object - * @name ngMock.$httpBackend - * @description - * Fake HTTP backend implementation suitable for unit testing application that use the - * {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less - * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. - * - * During unit testing, we want our unit tests to run quickly and have no external dependencies so - * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is - * to verify whether a certain request has been sent or not, or alternatively just let the - * application make requests, respond with pre-trained responses and assert that the end result is - * what we expect it to be. - * - * This mock implementation can be used to respond with static or dynamic responses via the - * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). - * - * When an Angular application needs some data from a server, it calls the $http service, which - * sends the request to a real server using $httpBackend service. With dependency injection, it is - * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to real server. - * - * There are two ways to specify what test data should be returned as http responses by the mock - * backend when the code under test makes http requests: - * - * - `$httpBackend.expect` - specifies a request expectation - * - `$httpBackend.when` - specifies a backend definition - * - * - * # Request Expectations vs Backend Definitions - * - * Request expectations provide a way to make assertions about requests made by the application and - * to define responses for those requests. The test will fail if the expected requests are not made - * or they are made in the wrong order. - * - * Backend definitions allow you to define a fake backend for your application which doesn't assert - * if a particular request was made or not, it just returns a trained response if a request is made. - * The test will pass whether or not the request gets made during testing. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
- * - * In cases where both backend definitions and request expectations are specified during unit - * testing, the request expectations are evaluated first. - * - * If a request expectation has no response specified, the algorithm will search your backend - * definitions for an appropriate response. - * - * If a request didn't match any expectation or if the expectation doesn't have the response - * defined, the backend definitions are evaluated in sequential order to see if any of them match - * the request. The response from the first matched definition is returned. - * - * - * # Flushing HTTP requests - * - * The $httpBackend used in production, always responds to requests with responses asynchronously. - * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are - * hard to write, follow and maintain. At the same time the testing mock, can't respond - * synchronously because that would change the execution of the code under test. For this reason the - * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending - * requests and thus preserving the async api of the backend, while allowing the test to execute - * synchronously. - * - * - * # Unit testing with mock $httpBackend - * - *
-   // controller
-   function MyController($scope, $http) {
-     $http.get('/auth.py').success(function(data) {
-       $scope.user = data;
-     });
-
-     this.saveMessage = function(message) {
-       $scope.status = 'Saving...';
-       $http.post('/add-msg.py', message).success(function(response) {
-         $scope.status = '';
-       }).error(function() {
-         $scope.status = 'ERROR!';
-       });
-     };
-   }
-
-   // testing controller
-   var $http;
-
-   beforeEach(inject(function($injector) {
-     $httpBackend = $injector.get('$httpBackend');
-
-     // backend definition common for all tests
-     $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
-   }));
-
-
-   afterEach(function() {
-     $httpBackend.verifyNoOutstandingExpectation();
-     $httpBackend.verifyNoOutstandingRequest();
-   });
-
-
-   it('should fetch authentication token', function() {
-     $httpBackend.expectGET('/auth.py');
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-   });
-
-
-   it('should send msg to server', function() {
-     // now you don’t care about the authentication, but
-     // the controller will still send the request and
-     // $httpBackend will respond without you having to
-     // specify the expectation and response for this request
-     $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
-
-     var controller = scope.$new(MyController);
-     $httpBackend.flush();
-     controller.saveMessage('message content');
-     expect(controller.status).toBe('Saving...');
-     $httpBackend.flush();
-     expect(controller.status).toBe('');
-   });
-
-
-   it('should send auth header', function() {
-     $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
-       // check if the header was send, if it wasn't the expectation won't
-       // match the request and the test will fail
-       return headers['Authorization'] == 'xxx';
-     }).respond(201, '');
-
-     var controller = scope.$new(MyController);
-     controller.saveMessage('whatever');
-     $httpBackend.flush();
-   });
-   
- */ -angular.mock.$HttpBackendProvider = function() { - this.$get = [createHttpBackendMock]; -}; - -/** - * General factory function for $httpBackend mock. - * Returns instance for unit testing (when no arguments specified): - * - passing through is disabled - * - auto flushing is disabled - * - * Returns instance for e2e testing (when `$delegate` and `$browser` specified): - * - passing through (delegating request to real backend) is enabled - * - auto flushing is enabled - * - * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) - * @param {Object=} $browser Auto-flushing enabled if specified - * @return {Object} Instance of $httpBackend mock - */ -function createHttpBackendMock($delegate, $browser) { - var definitions = [], - expectations = [], - responses = [], - responsesPush = angular.bind(responses, responses.push); - - function createResponse(status, data, headers) { - if (angular.isFunction(status)) return status; - - return function() { - return angular.isNumber(status) - ? [status, data, headers] - : [200, status, data]; - }; - } - - // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers) { - var xhr = new MockXhr(), - expectation = expectations[0], - wasExpected = false; - - function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) - ? data - : angular.toJson(data); - } - - if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) - throw Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); - - if (!expectation.matchHeaders(headers)) - throw Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); - - expectations.shift(); - - if (expectation.response) { - responses.push(function() { - var response = expectation.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); - }); - return; - } - wasExpected = true; - } - - var i = -1, definition; - while ((definition = definitions[++i])) { - if (definition.match(method, url, data, headers || {})) { - if (definition.response) { - // if $browser specified, we do auto flush all requests - ($browser ? $browser.defer : responsesPush)(function() { - var response = definition.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(response[0], response[1], xhr.getAllResponseHeaders()); - }); - } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers); - } else throw Error('No response defined !'); - return; - } - } - throw wasExpected ? - Error('No response defined !') : - Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); - } - - /** - * @ngdoc method - * @name ngMock.$httpBackend#when - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), - chain = { - respond: function(status, data, headers) { - definition.response = createResponse(status, data, headers); - } - }; - - if ($browser) { - chain.passThrough = function() { - definition.passThrough = true; - }; - } - - definitions.push(definition); - return chain; - }; - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenGET - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenHEAD - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenDELETE - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenPOST - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenPUT - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#whenJSONP - * @methodOf ngMock.$httpBackend - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('when'); - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expect - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers); - expectations.push(expectation); - return { - respond: function(status, data, headers) { - expectation.response = createResponse(status, data, headers); - } - }; - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectGET - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for GET requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. See #expect for more info. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectHEAD - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for HEAD requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectDELETE - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for DELETE requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPOST - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for POST requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPUT - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for PUT requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectPATCH - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for PATCH requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name ngMock.$httpBackend#expectJSONP - * @methodOf ngMock.$httpBackend - * @description - * Creates a new request expectation for JSONP requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('expect'); - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#flush - * @methodOf ngMock.$httpBackend - * @description - * Flushes all pending requests using the trained responses. - * - * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, - * all pending requests will be flushed. If there are no pending requests when the flush method - * is called an exception is thrown (as this typically a sign of programming error). - */ - $httpBackend.flush = function(count) { - if (!responses.length) throw Error('No pending request to flush !'); - - if (angular.isDefined(count)) { - while (count--) { - if (!responses.length) throw Error('No more pending request to flush !'); - responses.shift()(); - } - } else { - while (responses.length) { - responses.shift()(); - } - } - $httpBackend.verifyNoOutstandingExpectation(); - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingExpectation - * @methodOf ngMock.$httpBackend - * @description - * Verifies that all of the requests defined via the `expect` api were made. If any of the - * requests were not made, verifyNoOutstandingExpectation throws an exception. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - *
-   *   afterEach($httpBackend.verifyExpectations);
-   * 
- */ - $httpBackend.verifyNoOutstandingExpectation = function() { - if (expectations.length) { - throw Error('Unsatisfied requests: ' + expectations.join(', ')); - } - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#verifyNoOutstandingRequest - * @methodOf ngMock.$httpBackend - * @description - * Verifies that there are no outstanding requests that need to be flushed. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - *
-   *   afterEach($httpBackend.verifyNoOutstandingRequest);
-   * 
- */ - $httpBackend.verifyNoOutstandingRequest = function() { - if (responses.length) { - throw Error('Unflushed requests: ' + responses.length); - } - }; - - - /** - * @ngdoc method - * @name ngMock.$httpBackend#resetExpectations - * @methodOf ngMock.$httpBackend - * @description - * Resets all request expectations, but preserves all backend definitions. Typically, you would - * call resetExpectations during a multiple-phase test when you want to reuse the same instance of - * $httpBackend mock. - */ - $httpBackend.resetExpectations = function() { - expectations.length = 0; - responses.length = 0; - }; - - return $httpBackend; - - - function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers) - } - }); - - angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers) - } - }); - } -} - -function MockHttpExpectation(method, url, data, headers) { - - this.data = data; - this.headers = headers; - - this.match = function(m, u, d, h) { - if (method != m) return false; - if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; - return true; - }; - - this.matchUrl = function(u) { - if (!url) return true; - if (angular.isFunction(url.test)) return url.test(u); - return url == u; - }; - - this.matchHeaders = function(h) { - if (angular.isUndefined(headers)) return true; - if (angular.isFunction(headers)) return headers(h); - return angular.equals(headers, h); - }; - - this.matchData = function(d) { - if (angular.isUndefined(data)) return true; - if (data && angular.isFunction(data.test)) return data.test(d); - if (data && !angular.isString(data)) return angular.toJson(data) == d; - return data == d; - }; - - this.toString = function() { - return method + ' ' + url; - }; -} - -function MockXhr() { - - // hack for testing $http, $httpBackend - MockXhr.$$lastInstance = this; - - this.open = function(method, url, async) { - this.$$method = method; - this.$$url = url; - this.$$async = async; - this.$$reqHeaders = {}; - this.$$respHeaders = {}; - }; - - this.send = function(data) { - this.$$data = data; - }; - - this.setRequestHeader = function(key, value) { - this.$$reqHeaders[key] = value; - }; - - this.getResponseHeader = function(name) { - // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last - var header = this.$$respHeaders[name]; - if (header) return header; - - name = angular.lowercase(name); - header = this.$$respHeaders[name]; - if (header) return header; - - header = undefined; - angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) == name) header = headerVal; - }); - return header; - }; - - this.getAllResponseHeaders = function() { - var lines = []; - - angular.forEach(this.$$respHeaders, function(value, key) { - lines.push(key + ': ' + value); - }); - return lines.join('\n'); - }; - - this.abort = angular.noop; -} - - -/** - * @ngdoc function - * @name ngMock.$timeout - * @description - * - * This service is just a simple decorator for {@link ng.$timeout $timeout} service - * that adds a "flush" method. - */ - -/** - * @ngdoc method - * @name ngMock.$timeout#flush - * @methodOf ngMock.$timeout - * @description - * - * Flushes the queue of pending tasks. - */ - -/** - * - */ -angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
'); - } -}; - -/** - * @ngdoc overview - * @name ngMock - * @description - * - * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful - * mocks to the {@link AUTO.$injector $injector}. - */ -angular.module('ngMock', ['ng']).provider({ - $browser: angular.mock.$BrowserProvider, - $exceptionHandler: angular.mock.$ExceptionHandlerProvider, - $log: angular.mock.$LogProvider, - $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider -}).config(function($provide) { - $provide.decorator('$timeout', function($delegate, $browser) { - $delegate.flush = function() { - $browser.defer.flush(); - }; - return $delegate; - }); -}); - - -/** - * @ngdoc overview - * @name ngMockE2E - * @description - * - * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. - * Currently there is only one mock present in this module - - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. - */ -angular.module('ngMockE2E', ['ng']).config(function($provide) { - $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}); - -/** - * @ngdoc object - * @name ngMockE2E.$httpBackend - * @description - * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of - * applications that use the {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for unit testing please see - * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. - * - * This implementation can be used to respond with static or dynamic responses via the `when` api - * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the - * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch - * templates from a webserver). - * - * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application - * is being developed with the real backend api replaced with a mock, it is often desirable for - * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch - * templates or static files from the webserver). To configure the backend with this behavior - * use the `passThrough` request handler of `when` instead of `respond`. - * - * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests - * automatically, closely simulating the behavior of the XMLHttpRequest object. - * - * To setup the application to run with this http backend, you have to create a module that depends - * on the `ngMockE2E` and your application modules and defines the fake backend: - * - *
- *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
- *   myAppDev.run(function($httpBackend) {
- *     phones = [{name: 'phone1'}, {name: 'phone2'}];
- *
- *     // returns the current list of phones
- *     $httpBackend.whenGET('/phones').respond(phones);
- *
- *     // adds a new phone to the phones array
- *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
- *       phones.push(angular.fromJSON(data));
- *     });
- *     $httpBackend.whenGET(/^\/templates\//).passThrough();
- *     //...
- *   });
- * 
- * - * Afterwards, bootstrap your app with this new module. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#when - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - * - * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` - * handler, will be pass through to the real backend (an XHR request will be made to the - * server. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenGET - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenHEAD - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenDELETE - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPOST - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPUT - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenPATCH - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for PATCH requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name ngMockE2E.$httpBackend#whenJSONP - * @methodOf ngMockE2E.$httpBackend - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ -angular.mock.e2e = {}; -angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; - - -angular.mock.clearDataCache = function() { - var key, - cache = angular.element.cache; - - for(key in cache) { - if (cache.hasOwnProperty(key)) { - var handle = cache[key].handle; - - handle && angular.element(handle.elem).unbind(); - delete cache[key]; - } - } -}; - - -window.jstestdriver && (function(window) { - /** - * Global method to output any number of objects into JSTD console. Useful for debugging. - */ - window.dump = function() { - var args = []; - angular.forEach(arguments, function(arg) { - args.push(angular.mock.dump(arg)); - }); - jstestdriver.console.log.apply(jstestdriver.console, args); - if (window.console) { - window.console.log.apply(window.console, args); - } - }; -})(window); - - -window.jasmine && (function(window) { - - afterEach(function() { - var spec = getCurrentSpec(); - var injector = spec.$injector; - - spec.$injector = null; - spec.$modules = null; - - if (injector) { - injector.get('$rootElement').unbind(); - injector.get('$browser').pollFns.length = 0; - } - - angular.mock.clearDataCache(); - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - - function getCurrentSpec() { - return jasmine.getEnv().currentSpec; - } - - function isSpecRunning() { - var spec = getCurrentSpec(); - return spec && spec.queue.running; - } - - /** - * @ngdoc function - * @name angular.mock.module - * @description - * - * *NOTE*: This is function is also published on window for easy access.
- * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. - * - * This function registers a module configuration code. It collects the configuration information - * which will be used when the injector is created by {@link angular.mock.inject inject}. - * - * See {@link angular.mock.inject inject} for usage example - * - * @param {...(string|Function)} fns any number of modules which are represented as string - * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. - */ - window.module = angular.mock.module = function() { - var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - var spec = getCurrentSpec(); - if (spec.$injector) { - throw Error('Injector already created, can not register a module!'); - } else { - var modules = spec.$modules || (spec.$modules = []); - angular.forEach(moduleFns, function(module) { - modules.push(module); - }); - } - } - }; - - /** - * @ngdoc function - * @name angular.mock.inject - * @description - * - * *NOTE*: This is function is also published on window for easy access.
- * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. - * - * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link AUTO.$injector $injector} per test, which is then used for - * resolving references. - * - * See also {@link angular.mock.module module} - * - * Example of what a typical jasmine tests looks like with the inject method. - *
-   *
-   *   angular.module('myApplicationModule', [])
-   *       .value('mode', 'app')
-   *       .value('version', 'v1.0.1');
-   *
-   *
-   *   describe('MyApp', function() {
-   *
-   *     // You need to load modules that you want to test,
-   *     // it loads only the "ng" module by default.
-   *     beforeEach(module('myApplicationModule'));
-   *
-   *
-   *     // inject() is used to inject arguments of all given functions
-   *     it('should provide a version', inject(function(mode, version) {
-   *       expect(version).toEqual('v1.0.1');
-   *       expect(mode).toEqual('app');
-   *     }));
-   *
-   *
-   *     // The inject and module method can also be used inside of the it or beforeEach
-   *     it('should override a version and test the new version is injected', function() {
-   *       // module() takes functions or strings (module aliases)
-   *       module(function($provide) {
-   *         $provide.value('version', 'overridden'); // override version here
-   *       });
-   *
-   *       inject(function(version) {
-   *         expect(version).toEqual('overridden');
-   *       });
-   *     ));
-   *   });
-   *
-   * 
- * - * @param {...Function} fns any number of functions which will be injected using the injector. - */ - window.inject = angular.mock.inject = function() { - var blockFns = Array.prototype.slice.call(arguments, 0); - var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - var spec = getCurrentSpec(); - var modules = spec.$modules || []; - modules.unshift('ngMock'); - modules.unshift('ng'); - var injector = spec.$injector; - if (!injector) { - injector = spec.$injector = angular.injector(modules); - } - for(var i = 0, ii = blockFns.length; i < ii; i++) { - try { - injector.invoke(blockFns[i] || angular.noop, this); - } catch (e) { - if(e.stack) e.stack += '\n' + errorForStack.stack; - throw e; - } finally { - errorForStack = null; - } - } - } - }; -})(window); diff --git a/app/assets/javascripts/shared/angular-resource.js b/app/assets/javascripts/shared/angular-resource.js deleted file mode 100644 index 816107d241..0000000000 --- a/app/assets/javascripts/shared/angular-resource.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - AngularJS v1.0.3 - (c) 2010-2012 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(A,e,w){'use strict';e.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(x,y){function k(a,f){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(f?null:/%20/g,"+")}function t(a,f){this.template=a+="#";this.defaults=f||{};var b=this.urlParams={};l(a.split(/\W/),function(d){d&&a.match(RegExp("[^\\\\]:"+d+"\\W"))&&(b[d]=!0)});this.template=a.replace(/\\:/g,":")}function u(a,f,b){function d(b,c){var a= -{},c=i({},f,c);l(c,function(o,c){var d;o.charAt&&o.charAt(0)=="@"?(d=o.substr(1),d=y(d)(b)):d=o;a[c]=d});return a}function h(a){v(a||{},this)}var e=new t(a),b=i({},z,b);l(b,function(g,c){var k=g.method=="POST"||g.method=="PUT"||g.method=="PATCH";h[c]=function(a,b,c,f){var s={},j,m=p,q=null;switch(arguments.length){case 4:q=f,m=c;case 3:case 2:if(r(b)){if(r(a)){m=a;q=b;break}m=b;q=c}else{s=a;j=b;m=c;break}case 1:r(a)?m=a:k?j=a:s=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ -arguments.length+" arguments.";}var n=this instanceof h?this:g.isArray?[]:new h(j);x({method:g.method,url:e.url(i({},d(j,g.params||{}),s)),data:j}).then(function(a){var b=a.data;if(b)g.isArray?(n.length=0,l(b,function(a){n.push(new h(a))})):v(b,n);(m||p)(n,a.headers)},q);return n};h.bind=function(c){return u(a,i({},f,c),b)};h.prototype["$"+c]=function(a,b,f){var g=d(this),e=p,j;switch(arguments.length){case 3:g=a;e=b;j=f;break;case 2:case 1:r(a)?(e=a,j=b):(g=a,e=b||p);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ -arguments.length+" arguments.";}h[c].call(this,g,k?this:w,e,j)}});return h}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},p=e.noop,l=e.forEach,i=e.extend,v=e.copy,r=e.isFunction;t.prototype={url:function(a){var f=this,b=this.template,d,h,a=a||{};l(this.urlParams,function(g,c){d=a.hasOwnProperty(c)?a[c]:f.defaults[c];e.isDefined(d)&&d!==null?(h=k(d,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"), -b=b.replace(RegExp(":"+c+"(\\W)","g"),h+"$1")):b=b.replace(RegExp("/?:"+c+"(\\W)","g"),"$1")});var b=b.replace(/\/?#$/,""),i=[];l(a,function(a,b){f.urlParams[b]||i.push(k(b)+"="+k(a))});i.sort();b=b.replace(/\/*$/,"");return b+(i.length?"?"+i.join("&"):"")}};return u}])})(window,window.angular); diff --git a/app/assets/javascripts/shared/angular.js b/app/assets/javascripts/shared/angular.js deleted file mode 100644 index 07f501be38..0000000000 --- a/app/assets/javascripts/shared/angular.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - AngularJS v1.0.3 - (c) 2010-2012 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(U,ca,p){'use strict';function m(b,a,c){var d;if(b)if(N(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(L(b)&&wa(b.length))for(d=0;d=0&&b.splice(c,1);return a}function V(b,a){if(oa(b)||b&&b.$evalAsync&&b.$watch)throw B("Can't copy Window or Scope");if(a){if(b=== -a)throw B("Can't copy equivalent objects or arrays");if(J(b)){for(;a.length;)a.pop();for(var c=0;c2?ia.call(arguments,2):[];return N(a)&&!(a instanceof RegExp)?c.length? -function(){return arguments.length?a.apply(b,c.concat(ia.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function ic(b,a){var c=a;/^\$+/.test(b)?c=p:oa(a)?c="$WINDOW":a&&ca===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function da(b,a){return JSON.stringify(b,ic,a?" ":null)}function nb(b){return F(b)?JSON.parse(b):b}function Wa(b){b&&b.length!==0?(b=E(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1; -return b}function pa(b){b=u(b).clone();try{b.html("")}catch(a){}return u("
").append(b).html().match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+E(b)})}function Xa(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]),a[d]=v(c[1])?decodeURIComponent(c[1]):!0)});return a}function ob(b){var a=[];m(b,function(b,d){a.push(Ya(d,!0)+(b===!0?"":"="+Ya(b,!0)))});return a.length?a.join("&"):""}function Za(b){return Ya(b,!0).replace(/%26/gi,"&").replace(/%3D/gi, -"=").replace(/%2B/gi,"+")}function Ya(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(a?null:/%20/g,"+")}function jc(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,i=["ng:app","ng-app","x-ng-app","data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(i,function(a){i[a]=!0;c(ca.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+ -a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&i[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function pb(b,a){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");var c=qb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,i){a.$apply(function(){b.data("$injector",i);c(b)(a)})}]);return c}function $a(b,a){a=a||"_";return b.replace(kc, -function(b,d){return(d?a:"")+b.toLowerCase()})}function qa(b,a,c){if(!b)throw new B("Argument '"+(a||"?")+"' is "+(c||"required"));return b}function ra(b,a,c){c&&J(b)&&(b=b[b.length-1]);qa(N(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function lc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c, -d,e){return function(){b[e||"push"]([c,d,arguments]);return j}}if(!e)throw B("No module: "+d);var b=[],c=[],k=a("$injector","invoke"),j={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:k,run:function(a){c.push(a); -return this}};g&&k(g);return j})}})}function rb(b){return b.replace(mc,function(a,b,d,e){return e?d.toUpperCase():d}).replace(nc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,i,f,h,k,j,l;b.length;){i=b.shift();f=0;for(h=i.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function sa(b){sb(b);for(var a=0,b=b.childNodes||[];a-1}function vb(b,a){a&&m(a.split(" "),function(a){b.className= -R((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+R(a)+" "," "))})}function wb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=R(b.className+" "+R(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&v(a.length)&&!oa(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"),W.length>20&&c.warn("Cookie '"+a+"' possibly not set or overflowed because too many cookies were already set ("+W.length+ -" > 20 )")}else{if(h.cookie!==y){y=h.cookie;d=y.split("; ");W={};for(f=0;f0&&(W[unescape(e.substring(0,k))]=unescape(e.substring(k+1)))}return W}};f.defer=function(a,b){var c;n++;c=l(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};f.defer.cancel=function(a){return r[a]?(delete r[a],o(a),e(D),!0):!1}}function wc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new vc(b,d,a,c)}]}function xc(){this.$get=function(){function b(b, -d){function e(a){if(a!=l){if(o){if(o==a)o=a.n}else o=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw B("cacheId "+b+" taken");var i=0,f=x({},d,{id:b}),h={},k=d&&d.capacity||Number.MAX_VALUE,j={},l=null,o=null;return a[b]={put:function(a,b){var c=j[a]||(j[a]={key:a});e(c);t(b)||(a in h||i++,h[a]=b,i>k&&this.remove(o.key))},get:function(a){var b=j[a];if(b)return e(b),h[a]},remove:function(a){var b=j[a];if(b){if(b==l)l=b.p;if(b==o)o=b.n;g(b.n,b.p);delete j[a]; -delete h[a];i--}},removeAll:function(){h={};i=0;j={};l=o=null},destroy:function(){j=f=h=null;delete a[b]},info:function(){return x({},f,{size:i})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function yc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Bb(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: "; -this.directive=function f(d,e){F(d)?(qa(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d],function(a){try{var f=b.invoke(a);if(N(f))f={compile:I(f)};else if(!f.compile&&f.link)f.compile=I(f.link);f.priority=f.priority||0;f.name=f.name||d;f.require=f.require||f.controller&&f.name;f.restrict=f.restrict||"A";e.push(f)}catch(k){c(k)}});return e}])),a[d].push(e)):m(d,mb(f));return this};this.$get=["$injector","$interpolate","$exceptionHandler", -"$http","$templateCache","$parse","$controller","$rootScope",function(b,h,k,j,l,o,r,n){function w(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&(a[c]=u(b).wrap("").parent()[0])});var d=s(a,b,a,c);return function(b,c){qa(b,"scope");var e=c?ua.clone.call(a):a;e.data("$scope",b);q(e,"ng-scope");c&&c(e,b);d&&d(b,e,e);return e}}function q(a,b){try{a.addClass(b)}catch(c){}}function s(a,b,c,d){function e(a,c,d,k){for(var g,h,j,n,o,l=0,r=0,q=f.length;lz.priority)break;if(Y=z.scope)M("isolated scope",C,z,y),L(Y)&&(q(y,"ng-isolate-scope"),C=z),q(y,"ng-scope"),s=s||z;H=z.name;if(Y=z.controller)t=t||{},M("'"+H+"' controller",t[H],z,y),t[H]=z;if(Y=z.transclude)M("transclusion",D,z,y),D=z,n=z.priority,Y=="element"?(X=u(b),y=c.$$element=u("<\!-- "+H+": "+c[H]+" --\>"),b=y[0],Ga(e,u(X[0]),b),v=w(X,d,n)):(X=u(cb(b)).contents(),y.html(""),v=w(X,d));if(Y=z.template)if(M("template",A,z,y),A=z,Y=Ha(Y),z.replace){X=u("
"+R(Y)+"
").contents(); -b=X[0];if(X.length!=1||b.nodeType!==1)throw new B(g+Y);Ga(e,y,b);H={$attr:{}};a=a.concat(O(b,a.splice(E+1,a.length-(E+1)),H));K(c,H);G=a.length}else y.html(Y);if(z.templateUrl)M("template",A,z,y),A=z,j=W(a.splice(E,a.length-E),j,y,c,e,z.replace,v),G=a.length;else if(z.compile)try{x=z.compile(y,c,v),N(x)?f(null,x):x&&f(x.pre,x.post)}catch(I){k(I,pa(y))}if(z.terminal)j.terminal=!0,n=Math.max(n,z.priority)}j.scope=s&&s.scope;j.transclude=D&&v;return j}function A(d,e,g,h){var j=!1;if(a.hasOwnProperty(e))for(var n, -e=b.get(e+c),o=0,l=e.length;on.priority)&&n.restrict.indexOf(g)!=-1)d.push(n),j=!0}catch(r){k(r)}return j}function K(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,function(b,f){f=="class"?(q(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):f=="style"?e.attr("style",e.attr("style")+";"+b):f.charAt(0)!="$"&&!a.hasOwnProperty(f)&&(a[f]=b,d[f]=c[f])})}function W(a,b,c,d,e, -f,k){var h=[],n,o,r=c[0],q=a.shift(),w=x({},q,{controller:null,templateUrl:null,transclude:null,scope:null});c.html("");j.get(q.templateUrl,{cache:l}).success(function(j){var l,q,j=Ha(j);if(f){q=u("
"+R(j)+"
").contents();l=q[0];if(q.length!=1||l.nodeType!==1)throw new B(g+j);j={$attr:{}};Ga(e,c,l);O(l,a,j);K(d,j)}else l=r,c.html(j);a.unshift(w);n=C(a,c,d,k);for(o=s(c.contents(),k);h.length;){var ba=h.pop(),j=h.pop();q=h.pop();var y=h.pop(),m=l;q!==r&&(m=cb(l),Ga(j,u(q),m));n(function(){b(o, -y,m,e,ba)},y,m,e,ba)}h=null}).error(function(a,b,c,d){throw B("Failed to load template: "+d.url);});return function(a,c,d,e,f){h?(h.push(c),h.push(d),h.push(e),h.push(f)):n(function(){b(o,c,d,e,f)},c,d,e,f)}}function y(a,b){return b.priority-a.priority}function M(a,b,c,d){if(b)throw B("Multiple directives ["+b.name+", "+c.name+"] asking for "+a+" on: "+pa(d));}function H(a,b){var c=h(b,!0);c&&a.push({priority:0,compile:I(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);q(d.data("$binding", -e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function X(a,b,c,d){var e=h(c,!0);e&&b.push({priority:100,compile:I(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=h(c[d],!0));c[d]=p;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e,function(a){c.$set(d,a)})})})}function Ga(a,b,c){var d=b[0],e=d.parentNode,f,g;if(a){f=0;for(g=a.length;f0){var e=M[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=i(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),M.shift(),b):!1}function h(a){f(a)||e("is unexpected, expecting ["+a+"]",i())}function k(a,b){return function(c,d){return a(c,d,b)}}function j(a,b,c){return function(d,e){return b(d,e,a,c)}}function l(){for(var a=[];;)if(M.length>0&&!i("}",")",";","]")&&a.push(v()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d, -e=0;e","<=",">="))a=j(a,b.fn,q());return a}function s(){for(var a=m(),b;b=f("*","/","%");)a=j(a,b.fn,m());return a}function m(){var a;return f("+")?C():(a=f("-"))?j(W,a.fn,m()):(a=f("!"))?k(a.fn,m()):C()}function C(){var a;if(f("("))a=v(),h(")");else if(f("["))a=A();else if(f("{"))a=K();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=u(a,c),c=null):b.text==="["?(c=a,a=ea(a)):b.text==="."?(c=a,a=t(a)):e("IMPOSSIBLE"); -return a}function A(){var a=[];if(g().text!="]"){do a.push(H());while(f(","))}h("]");return function(b,c){for(var d=[],e=0;e1;d++){var e=a.shift(),g= -b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]=c}function fb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,i=0;i7),hasEvent:function(c){if(c=="input"&&aa==9)return!1;if(t(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Uc(){this.$get=I(U)}function Mb(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=E(R(b.substr(0, -e)));d=R(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Nb(b){var a=L(b)?b:p;return function(c){a||(a=Mb(b));return c?a[E(c)]||null:a}}function Ob(b,a,c){if(N(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Vc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){F(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=nb(d,!0)));return d}],transformRequest:[function(a){return L(a)&&Sa.apply(a)!=="[object File]"?da(a):a}], -headers:{common:{Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,h,k,j){function l(a){function c(a){var b=x({},a,{data:Ob(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:k.reject(b)}a.method=la(a.method);var e=a.transformRequest|| -d.transformRequest,f=a.transformResponse||d.transformResponse,h=d.headers,h=x({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},h.common,h[E(a.method)],a.headers),e=Ob(a.data,Nb(h),e),g;t(a.data)&&delete h["Content-Type"];g=o(a,e,h);g=g.then(c,c);m(w,function(a){g=a(g)});g.success=function(b){g.then(function(c){b(c.data,c.status,c.headers,a)});return g};g.error=function(b){g.then(null,function(c){b(c.data,c.status,c.headers,a)});return g};return g}function o(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(w, -[a,b,Mb(c)]):m.remove(w));f(b,a,c);h.$apply()}function f(a,c,d){c=Math.max(c,0);(200<=c&&c<300?j.resolve:j.reject)({data:a,status:c,headers:Nb(d),config:b})}function i(){var a=za(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var j=k.defer(),o=j.promise,m,p,w=r(b.url,b.params);l.pendingRequests.push(b);o.then(i,i);b.cache&&b.method=="GET"&&(m=L(b.cache)?b.cache:n);if(m)if(p=m.get(w))if(p.then)return p.then(i,i),p;else J(p)?f(p[1],p[0],V(p[2])):f(p,200,{});else m.put(w,o);p||a(b.method, -w,c,e,d,b.timeout,b.withCredentials);return o}function r(a,b){if(!b)return a;var c=[];ec(b,function(a,b){a==null||a==p||(L(a)&&(a=da(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var n=c("$http"),w=[];m(e,function(a){w.push(F(a)?j.get(a):j.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(x(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]= -function(b,c,d){return l(x(d||{},{method:a,url:b,data:c}))}})})("post","put");l.defaults=d;return l}]}function Wc(){this.$get=["$browser","$window","$document",function(b,a,c){return Xc(b,Yc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Xc(b,a,c,d,e,g){function i(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;aa?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= -d;e.body.appendChild(c)}return function(e,h,k,j,l,o,r){function n(a,c,d,e){c=(h.match(Fb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(D)}b.$$incOutstandingRequestCount();h=h||b.url();if(E(e)=="jsonp"){var p="_"+(d.counter++).toString(36);d[p]=function(a){d[p].data=a};i(h.replace("JSON_CALLBACK","angular.callbacks."+p),function(){d[p].data?n(j,200,d[p].data):n(j,-2);delete d[p]})}else{var q=new a;q.open(e,h,!0);m(l,function(a,b){a&&q.setRequestHeader(b,a)}); -var s;q.onreadystatechange=function(){q.readyState==4&&n(j,s||q.status,q.responseText,q.getAllResponseHeaders())};if(r)q.withCredentials=!0;q.send(k||"");o>0&&c(function(){s=-1;q.abort()},o)}}}function Zc(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"}, -DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","),SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a", -shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function $c(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,f,h){var k=c.defer(),j=k.promise,l=v(h)&&!h,f=a.defer(function(){try{k.resolve(e())}catch(a){k.reject(a),d(a)}l||b.$apply()},f),h=function(){delete g[j.$$timeoutId]};j.$$timeoutId=f;g[f]=k;j.then(h,h);return j}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)): -!1};return e}]}function Pb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Qb);a("date",Rb);a("filter",ad);a("json",bd);a("limitTo",cd);a("lowercase",dd);a("number",Sb);a("orderBy",Tb);a("uppercase",ed)}function ad(){return function(b,a){if(!(b instanceof Array))return b;var c=[];c.check=function(a){for(var b=0;b-1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;ce+1?i="0":(f=i,k=!0)}if(!k){i=(i.split(Vb)[1]||"").length;t(e)&&(e=Math.min(Math.max(a.minFrac,i),a.maxFrac));var i=Math.pow(10,e),b=Math.round(b*i)/i,b=(""+b).split(Vb),i=b[0],b=b[1]||"",k=0,j=a.lgSize,l=a.gSize;if(i.length>=j+l)for(var k=i.length-j,o=0;o0||e>-c)e+=c;e===0&&c==-12&&(e=12);return ib(e,a,d)}}function La(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Rb(b){function a(a){var b; -if(b=a.match(c)){var a=new Date(0),g=0,i=0;b[9]&&(g=G(b[9]+b[10]),i=G(b[9]+b[11]));a.setUTCFullYear(G(b[1]),G(b[2])-1,G(b[3]));a.setUTCHours(G(b[4]||0)-g,G(b[5]||0)-i,G(b[6]||0),G(b[7]||0))}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e){var g="",i=[],f,h,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;F(c)&&(c=fd.test(c)?G(c):a(c));wa(c)&&(c=new Date(c));if(!na(c))return c;for(;e;)(h=gd.exec(e))?(i=i.concat(ia.call(h, -1)),e=i.pop()):(i.push(e),e=null);m(i,function(a){f=hd[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function bd(){return function(b){return da(b,!0)}}function cd(){return function(b,a){if(!(b instanceof Array))return b;var a=G(a),c=[],d,e;if(!b||!(b instanceof Array))return c;a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dl?(d.$setValidity("maxlength", -!1),p):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function jb(b,a){b="ngClass"+b;return S(function(c,d,e){function g(b,d){if(a===!0||c.$index%2===a)d&&b!==d&&i(d),f(b)}function i(a){L(a)&&!J(a)&&(a=Ta(a,function(a,b){if(a)return b}));d.removeClass(J(a)?a.join(" "):a)}function f(a){L(a)&&!J(a)&&(a=Ta(a,function(a,b){if(a)return b}));a&&d.addClass(J(a)?a.join(" "):a)}c.$watch(e[b],g,!0);e.$observe("class",function(){var a=c.$eval(e[b]);g(a,a)});b!=="ngClass"&&c.$watch("$index", -function(d,g){var j=d%2;j!==g%2&&(j==a?f(c.$eval(e[b])):i(c.$eval(e[b])))})})}var E=function(b){return F(b)?b.toLowerCase():b},la=function(b){return F(b)?b.toUpperCase():b},B=U.Error,aa=G((/msie (\d+)/.exec(E(navigator.userAgent))||[])[1]),u,ja,ia=[].slice,Ra=[].push,Sa=Object.prototype.toString,Yb=U.angular||(U.angular={}),ta,Cb,Z=["0","0","0"];D.$inject=[];ma.$inject=[];Cb=aa<9?function(b){b=b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName? -b.nodeName:b[0].nodeName};var kc=/[A-Z]/g,id={full:"1.0.3",major:1,minor:0,dot:3,codeName:"bouncy-thunder"},Ba=Q.cache={},Aa=Q.expando="ng-"+(new Date).getTime(),oc=1,Zb=U.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},db=U.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},mc=/([\:\-\_]+(.))/g,nc=/^moz([A-Z])/,ua=Q.prototype={ready:function(b){function a(){c||(c=!0,b())} -var c=!1;this.bind("DOMContentLoaded",a);Q(U).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+b])},length:0,push:Ra,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[E(b)]=b});var zb={};m("input,select,option,textarea,button,form".split(","),function(b){zb[la(b)]=!0});m({data:ub,inheritedData:Da,scope:function(b){return Da(b, -"$scope")},controller:xb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=rb(a);if(v(c))b.style[a]=c;else{var d;aa<=8&&(d=b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];aa<=8&&(d=d===""?p:d);return d}},attr:function(b,a,c){var d=E(a);if(Ea[d])if(v(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||D).specified?d:p;else if(v(c))b.setAttribute(a, -c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?p:b},prop:function(b,a,c){if(v(c))b[a]=c;else return b[a]},text:x(aa<9?function(b,a){if(b.nodeType==1){if(t(a))return b.innerText;b.innerText=a}else{if(t(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(t(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(t(a))return b.value;b.value=a},html:function(b,a){if(t(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a, -c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Lc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},hb={},Yc=U.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw new B("This browser does not support XMLHttpRequest."); -};Pb.$inject=["$provide"];Qb.$inject=["$locale"];Sb.$inject=["$locale"];var Vb=".",hd={yyyy:P("FullYear",4),yy:P("FullYear",2,0,!0),y:P("FullYear",1),MMMM:La("Month"),MMM:La("Month",!0),MM:P("Month",2,1),M:P("Month",1,1),dd:P("Date",2),d:P("Date",1),HH:P("Hours",2),H:P("Hours",1),hh:P("Hours",2,-12),h:P("Hours",1,-12),mm:P("Minutes",2),m:P("Minutes",1),ss:P("Seconds",2),s:P("Seconds",1),EEEE:La("Day"),EEE:La("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=a.getTimezoneOffset(); -return ib(a/60,2)+ib(Math.abs(a%60),2)}},gd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,fd=/^\d+$/;Rb.$inject=["$locale"];var dd=I(E),ed=I(la);Tb.$inject=["$parse"];var jd=I({restrict:"E",compile:function(a,c){c.href||c.$set("href","");return function(a,c){c.bind("click",function(a){if(!c.attr("href"))return a.preventDefault(),!1})}}}),kb={};m(Ea,function(a,c){var d=fa("ng-"+c);kb[d]=function(){return{priority:100,compile:function(){return function(a,g,i){a.$watch(i[d], -function(a){i.$set(c,!!a)})}}}}});m(["src","href"],function(a){var c=fa("ng-"+a);kb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){c&&(g.$set(a,c),aa&&e.prop(a,c))})}}}});var Oa={$addControl:D,$removeControl:D,$setValidity:D,$setDirty:D};Wb.$inject=["$element","$attrs","$scope"];var Ra=function(a){return["$timeout",function(c){var d={name:"form",restrict:"E",controller:Wb,compile:function(){return{pre:function(a,d,i,f){if(!i.action){var h=function(a){a.preventDefault? -a.preventDefault():a.returnValue=!1};Zb(d[0],"submit",h);d.bind("$destroy",function(){c(function(){db(d[0],"submit",h)},0,!1)})}var k=d.parent().controller("form"),j=i.name||i.ngForm;j&&(a[j]=f);k&&d.bind("$destroy",function(){k.$removeControl(f);j&&(a[j]=p);x(f,Oa)})}}}};return a?x(V(d),{restrict:"EAC"}):d}]},kd=Ra(),ld=Ra(!0),md=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,nd=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,od=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/, -ac={text:Qa,number:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);e.$parsers.push(function(a){var c=T(a);return c||od.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number",!1),p)});e.$formatters.push(function(a){return T(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!T(a)&&ah?(e.$setValidity("max", -!1),p):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return T(a)||wa(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1),p)})},url:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);a=function(a){return T(a)||md.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,g,i){Qa(a,c,d,e,g,i);a=function(a){return T(a)||nd.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email", -!1),p)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){t(d.name)&&c.attr("name",xa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,i=d.ngFalseValue;F(g)||(g=!0);F(i)||(i=!1);c.bind("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a=== -g});e.$parsers.push(function(a){return a?g:i})},hidden:D,button:D,submit:D,reset:D},bc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,g,i){i&&(ac[E(g.type)]||ac.text)(d,e,g,i,c,a)}}}],Na="ng-valid",Ma="ng-invalid",Pa="ng-pristine",Xb="ng-dirty",pd=["$scope","$exceptionHandler","$attrs","$element","$parse",function(a,c,d,e,g){function i(a,c){c=c?"-"+$a(c,"-"):"";e.removeClass((a?Ma:Na)+c).addClass((a?Na:Ma)+c)}this.$modelValue=this.$viewValue=Number.NaN; -this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var f=g(d.ngModel),h=f.assign;if(!h)throw B(Db+d.ngModel+" ("+pa(e)+")");this.$render=D;var k=e.inheritedData("$formController")||Oa,j=0,l=this.$error={};e.addClass(Pa);i(!0);this.$setValidity=function(a,c){if(l[a]!==!c){if(c){if(l[a]&&j--,!j)i(!0),this.$valid=!0,this.$invalid=!1}else i(!1),this.$invalid=!0,this.$valid=!1,j++;l[a]=!c;i(c,a);k.$setValidity(a, -c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Pa).addClass(Xb),k.$setDirty();m(this.$parsers,function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,h(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var o=this;a.$watch(function(){var c=f(a);if(o.$modelValue!==c){var d=o.$formatters,e=d.length;for(o.$modelValue=c;e--;)c=d[e](c);if(o.$viewValue!==c)o.$viewValue=c,o.$render()}})}],qd=function(){return{require:["ngModel", -"^?form"],controller:pd,link:function(a,c,d,e){var g=e[0],i=e[1]||Oa;i.$addControl(g);c.bind("$destroy",function(){i.$removeControl(g)})}}},rd=I({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),cc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var g=function(a){if(d.required&&(T(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g); -d.$observe("required",function(){g(e.$viewValue)})}}}},sd=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(R(a))});return c});e.$formatters.push(function(a){return J(a)?a.join(", "):p})}}},td=/^(true|false|\d+)$/,ud=function(){return{priority:100,compile:function(a,c){return td.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a, -c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},vd=S(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==p?"":a)})}),wd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],xd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe, -function(a){c.html(a||"")})}}],yd=jb("",!0),zd=jb("Odd",0),Ad=jb("Even",1),Bd=S({compile:function(a,c){c.$set("ngCloak",p);a.removeClass("ng-cloak")}}),Cd=[function(){return{scope:!0,controller:"@"}}],Dd=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],dc={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "),function(a){var c=fa("ng-"+a);dc[c]=["$parse",function(d){return function(e,g,i){var f=d(i[c]);g.bind(E(a),function(a){e.$apply(function(){f(e, -{$event:a})})})}}]});var Ed=S(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Fd=["$http","$templateCache","$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,i){var f=i.ngInclude||i.src,h=i.onload||"",k=i.autoscroll;return function(g,i){var o=0,m,n=function(){m&&(m.$destroy(),m=null);i.html("")};g.$watch(f,function(f){var p=++o;f?a.get(f,{cache:c}).success(function(a){p===o&&(m&&m.$destroy(),m=g.$new(),i.html(a),e(i.contents())(m), -v(k)&&(!k||g.$eval(k))&&d(),m.$emit("$includeContentLoaded"),g.$eval(h))}).error(function(){p===o&&n()}):n()})}}}}],Gd=S({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Hd=S({terminal:!0,priority:1E3}),Id=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,i){var f=i.count,h=g.attr(i.$attr.when),k=i.offset||0,j=e.$eval(h),l={},o=c.startSymbol(),r=c.endSymbol();m(j,function(a,e){l[e]=c(a.replace(d,o+f+"-"+k+r))});e.$watch(function(){var c= -parseFloat(e.$eval(f));return isNaN(c)?"":(j[c]||(c=a.pluralCat(c-k)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Jd=S({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a,c,i){var f=i.ngRepeat,i=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),h,k,j;if(!i)throw B("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f=i[1];h=i[2];i=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!i)throw B("'item' in 'item in collection' should be identifier or (key, value) but got '"+ -f+"'.");k=i[3]||i[1];j=i[2];var l=new eb;a.$watch(function(a){var e,f,i=a.$eval(h),m=gc(i,!0),p,u=new eb,C,A,v,t,y=c;if(J(i))v=i||[];else{v=[];for(C in i)i.hasOwnProperty(C)&&C.charAt(0)!="$"&&v.push(C);v.sort()}e=0;for(f=v.length;ex;)t.pop().element.remove()}for(;v.length>w;)v.pop()[0].element.remove()}var i;if(!(i=w.match(d)))throw B("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+w+"'.");var j=c(i[2]||i[1]),k=i[4]|| -i[6],l=i[5],m=c(i[3]||""),o=c(i[2]?i[1]:k),r=c(i[7]),v=[[{element:f,label:""}]];q&&(a(q)(e),q.removeClass("ng-scope"),q.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a,c=r(e)||[],d={},h,i,j,m,q,s;if(n){i=[];m=0;for(s=v.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); diff --git a/app/assets/javascripts/shared/bindonce.min.js b/app/assets/javascripts/shared/bindonce.min.js new file mode 100644 index 0000000000..0edd3d57d4 --- /dev/null +++ b/app/assets/javascripts/shared/bindonce.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};return l}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file diff --git a/app/assets/javascripts/shared/ng-infinite-scroll.min.js b/app/assets/javascripts/shared/ng-infinite-scroll.min.js new file mode 100644 index 0000000000..d4410b93c0 --- /dev/null +++ b/app/assets/javascripts/shared/ng-infinite-scroll.min.js @@ -0,0 +1,2 @@ +/* ng-infinite-scroll - v1.0.0 - 2013-02-23 */ +var mod;mod=angular.module("infinite-scroll",[]),mod.directive("infiniteScroll",["$rootScope","$window","$timeout",function(i,n,e){return{link:function(t,l,o){var r,c,f,a;return n=angular.element(n),f=0,null!=o.infiniteScrollDistance&&t.$watch(o.infiniteScrollDistance,function(i){return f=parseInt(i,10)}),a=!0,r=!1,null!=o.infiniteScrollDisabled&&t.$watch(o.infiniteScrollDisabled,function(i){return a=!i,a&&r?(r=!1,c()):void 0}),c=function(){var e,c,u,d;return d=n.height()+n.scrollTop(),e=l.offset().top+l.height(),c=e-d,u=n.height()*f>=c,u&&a?i.$$phase?t.$eval(o.infiniteScroll):t.$apply(o.infiniteScroll):u?r=!0:void 0},n.on("scroll",c),t.$on("$destroy",function(){return n.off("scroll",c)}),e(function(){return o.infiniteScrollImmediateCheck?t.$eval(o.infiniteScrollImmediateCheck)?c():void 0:c()},0)}}}]); \ No newline at end of file diff --git a/app/assets/javascripts/store/all.js b/app/assets/javascripts/store/all.js index 4da67e3567..59af65daa5 100644 --- a/app/assets/javascripts/store/all.js +++ b/app/assets/javascripts/store/all.js @@ -9,7 +9,7 @@ //= require store/spree_core //= require store/spree_auth //= require store/spree_promo -//= require shared/angular -//= require shared/angular-resource +//= require angular +//= require angular-resource //= require_tree . diff --git a/app/assets/stylesheets/darkswarm/active_table.css.sass b/app/assets/stylesheets/darkswarm/active_table.css.sass new file mode 100644 index 0000000000..c950cad28a --- /dev/null +++ b/app/assets/stylesheets/darkswarm/active_table.css.sass @@ -0,0 +1,103 @@ +@import branding +@import mixins +@import "compass/css3/user-interface" + +.active_table + margin: 2em 0em + @include user-select(none) + .active_table_row + padding: 0.8em 0.5em + display: block + &:first-child + cursor: pointer + + +.active_table .active_table_node + @include csstrans + display: block + border: 1px solid transparent + + .active_table_row // Inherits from active_table + border: 1px solid transparent + &, & > a.row + display: block + + &.open + .active_table_row:first-child + @include csstrans + border-top: 1px solid $dark-grey + border-left: 1px solid $dark-grey + border-right: 1px solid $dark-grey + border-bottom: none + -webkit-box-shadow: inset 0 1px 2px 0 rgba(0,0,0,0.5) + box-shadow: inset 0 1px 2px 0 rgba(0,0,0,0.5) + background-color: rgba(0,0,0,0.15) + &:hover, &:active, &:focus + color: $dark-grey + background-color: rgba(0,0,0,0.05) + + .active_table_row:nth-child(2) + border-left: 1px solid $dark-grey + border-right: 1px solid $dark-grey + border-bottom: 1px solid $dark-grey + border-top: none + background-color: rgba(255,255,255,0.2) + + .active_table_row.link + @include csstrans + padding: 0 + background-color: $dark-grey + -webkit-box-shadow: 0 1px 1px 0 rgba(0,0,0,0.35) + box-shadow: 0 1px 1px 0 rgba(0,0,0,0.35) + &:hover + background-color: $disabled-dark + a + display: block + padding: 0.8em 0.5em + margin: 0 -0.9375rem + &, & * + color: white + + p + padding-top: 1em + + p.trans-sentence + text-transform: capitalize + + &.closed + &:hover, &:active, &:focus + background-color: rgba(255,255,255,0.2) + border: 1px solid $dark-grey + color: $dark-grey + + &.inactive, &.inactive strong + color: $disabled-dark + &.closed + &:hover, &:active, &:focus + border: 1px solid $disabled-dark + color: $disabled-dark + &.open + .active_table_row:first-child + color: $dark-grey + border-top: 1px solid $disabled-dark + border-left: 1px solid $disabled-dark + border-right: 1px solid $disabled-dark + strong + color: $dark-grey + .active_table_row:nth-child(2) + border-left: 1px solid $disabled-dark + border-right: 1px solid $disabled-dark + border-bottom: 1px solid $disabled-dark + + &.current + &.closed + &, & * + color: $dark-grey + &.open + .active_table_row:first-child + &, & * + color: $dark-grey + + + + diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index 7c3f9de2d5..bbcdb15a3b 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -8,3 +8,7 @@ *= require_tree . */ @import 'foundation-icons'; + +ofn-modal { + display: block; +} diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass new file mode 100644 index 0000000000..5fb026cec4 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -0,0 +1,13 @@ +$clr-brick: #8f301d +$clr-brick-light: #f5e4e1 +$clr-brick-ultra-light: #f7f4ef +$clr-brick-bright: #db583d + +$clr-turquoise: #097563 +$clr-turquoise-light: #cef2ec +$clr-turquoise-ultra-light: #e6faf7 +$clr-turquoise-bright: #1d8f7c + +$disabled-dark: #999 +$disabled-bright: #ccc +$dark-grey: #333 \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index 676ab08e43..4b8065819a 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -4,3 +4,12 @@ checkout orderdetails .button, table width: 100% + + dd + i.fi-check, &.valid i.fi-x + display: none + &.valid i.fi-check + display: inline + + orderdetails table tr th + text-align: left diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index df649a8d59..3041817073 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -1,16 +1,12 @@ -@import variables +@import branding +@import mixins -#footer - padding: 74px 0px 0px - background: $fawn - margin-top: 85px - - #copyright - clear: both - img - display: block - margin: 0px auto 8px - - .contact - strong - padding-right: 1em +footer + background: $dark-grey + @include panepadding + .row + &, & * + color: white + span.email + direction: rtl + unicode-bidi: bidi-override diff --git a/app/assets/stylesheets/darkswarm/forms.css.sass b/app/assets/stylesheets/darkswarm/forms.css.sass deleted file mode 100644 index bada1fb68d..0000000000 --- a/app/assets/stylesheets/darkswarm/forms.css.sass +++ /dev/null @@ -1,16 +0,0 @@ -@import variables - -form - fieldset - padding: 0px - border: none - legend - border: 1px solid $dark-grey - border-left: 0px - border-right: 0px - padding: 16px 24px - display: block - width: 100% - margin-bottom: 1em - text-transform: uppercase - color: #999999 diff --git a/app/assets/stylesheets/darkswarm/home.css.sass b/app/assets/stylesheets/darkswarm/home.css.sass new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/stylesheets/darkswarm/home_panes.css.sass b/app/assets/stylesheets/darkswarm/home_panes.css.sass new file mode 100644 index 0000000000..810aaf3b17 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/home_panes.css.sass @@ -0,0 +1,32 @@ +@import branding +@import mixins + + +#beta + @include lightbg + .row + @include panepadding + +#map + @include darkbg + .row + @include panepadding + background-image: url("/assets/home/maps-bg.svg") + background-repeat: no-repeat + background-position: left center + +#groups + @include darkbg + .row + @include panepadding + background-image: url("/assets/home/groups-bg.svg") + background-repeat: no-repeat + background-position: left center + +#producers + @include turqbg + .row + @include panepadding + background-image: url("/assets/home/producers-bg.svg") + background-repeat: no-repeat + background-position: right center diff --git a/app/assets/stylesheets/darkswarm/hub_node.css.sass b/app/assets/stylesheets/darkswarm/hub_node.css.sass new file mode 100644 index 0000000000..f24a5e6302 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/hub_node.css.sass @@ -0,0 +1,40 @@ +@import branding + +.active_table .active_table_node + + &.open + .active_table_row:first-child + border-top: 1px solid $clr-brick + border-left: 1px solid $clr-brick + border-right: 1px solid $clr-brick + &:hover, &:active, &:focus + color: $clr-brick + + .active_table_row:nth-child(2) + border-left: 1px solid $clr-brick + border-right: 1px solid $clr-brick + border-bottom: 1px solid $clr-brick + + .active_table_row.link + background-color: $clr-brick + &:hover + background-color: $clr-brick-bright + + + &.closed + &:hover, &:active, &:focus + border: 1px solid $clr-brick + color: $clr-brick + + &.current + &.closed + &, & * + color: $clr-brick + &.open + .active_table_row:first-child + &, & * + color: $clr-brick + + + + diff --git a/app/assets/stylesheets/darkswarm/hub_search.css.sass b/app/assets/stylesheets/darkswarm/hub_search.css.sass new file mode 100644 index 0000000000..d1b28f9ad1 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/hub_search.css.sass @@ -0,0 +1,9 @@ +@import mixins + +#hub-search + input + font-size: 2em + @include big-input + //.advanced + //padding-top: 8px + //@include disabled diff --git a/app/assets/stylesheets/darkswarm/hubs.css.sass b/app/assets/stylesheets/darkswarm/hubs.css.sass new file mode 100644 index 0000000000..87ccb3ef50 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/hubs.css.sass @@ -0,0 +1,7 @@ +@import branding +@import mixins + +#hubs + background: $clr-brick-ultra-light url("/assets/home/shopping-bg.jpg") + @include fullwidthbg + @include panepadding diff --git a/app/assets/stylesheets/darkswarm/lists.css.sass b/app/assets/stylesheets/darkswarm/lists.css.sass new file mode 100644 index 0000000000..244d0dd862 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/lists.css.sass @@ -0,0 +1,7 @@ + +body ol + list-style-type: none + margin-left: 0em + padding-top: 1em + li + margin-left: 0 diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 70554bac29..1daabe34a5 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -1,8 +1,67 @@ @import typography +@import branding @mixin big-input border: 1px solid #999 font-size: 18px @extend .avenir - padding: 18px - margin-bottom: 1.25em + padding: 30px 20px + margin-bottom: 1em + +@mixin disabled + color: $disabled-bright + +@mixin panepadding + padding-top: 100px + padding-bottom: 100px + +@mixin darkbg + background-color: $clr-brick + &, & * + color: white + a + color: $clr-brick-ultra-light + &:hover + text-decoration: none + color: $clr-brick-light + +@mixin lightbg + background-color: $clr-brick-ultra-light + &, & * + color: black + a + color: $clr-brick + &:hover + text-decoration: none + color: $clr-brick-bright + +@mixin turqbg + background-color: $clr-turquoise-light + &, & * + color: $clr-turquoise + a + color: white + &:hover + text-decoration: none + color: $clr-turquoise-light + +@mixin fullbg + background-position: center center + background-repeat: no-repeat + -webkit-background-size: cover + -moz-background-size: cover + -o-background-size: cover + background-size: cover + +@mixin fullwidthbg + background-position: top center + background-repeat: no-repeat + background-size: 100% auto + +@mixin csstrans + -webkit-transition: all 100ms ease-in-out + -moz-transition: all 100ms ease-in-out + -ms-transition: all 100ms ease-in-out + -o-transition: all 100ms ease-in-out + transition: all 100ms ease-in-out + -webkit-transform-style: preserve-3d diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index e8a545b679..79368f5d67 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -61,7 +61,7 @@ product select width: 280px display: inline-block - background: transparent + vackground: transparent border-width: 2px border-color: #666666 font-size: 1em diff --git a/app/assets/stylesheets/darkswarm/tagline.css.sass b/app/assets/stylesheets/darkswarm/tagline.css.sass new file mode 100644 index 0000000000..b8a0d7cec5 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/tagline.css.sass @@ -0,0 +1,16 @@ +@import branding +@import mixins + +#tagline + background: black url("/assets/home/tagline-bg.jpg") + @include fullbg + height: 400px + padding: 40px 0px + h1, h2, p + color: white + h1 + margin-bottom: 1em + h2 + font-size: 1.6875rem + a + color: $clr-brick-bright \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index 35ab5a8db7..a75e4c29fd 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -1,3 +1,5 @@ +@import branding + @font-face font-family: 'AvenirBla_IE' src: url("/AveniBla.eot") format("opentype") @@ -18,9 +20,10 @@ //font-family: "AvenirBla_IE", "AvenirBla" a - color: #267D97 + color: $clr-brick &:hover - text-decoration: underline + text-decoration: none + color: $clr-brick-bright @mixin avenir color: #333333 diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass new file mode 100644 index 0000000000..ddb4ab5324 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -0,0 +1,39 @@ +@import foundation/components/buttons +@import branding +@import mixins + +.neutral-btn + @include button + background-color: transparent + border: 2px solid rgba(200, 200, 200, 1) + color: #999 + +.neutral-btn:hover, .neutral-btn:active, .neutral-btn:focus + background-color: rgba(200, 200, 200, 0.2) + border: 2px solid rgba(200, 200, 200, 0.8) + +.neutral-btn.dark + border-color: #000 + color: #000 + +.neutral-btn.dark:hover, .neutral-btn.dark:active, .neutral-btn.dark:focus + background-color: rgba(0, 0, 0, 0.1) + border: 2px solid rgba(0, 0, 0, 0.8) + text-shadow: 0 1px 0 #fff + +.neutral-btn.light + border-color: #fff + color: #fff + +.neutral-btn.light:hover, .neutral-btn.light:active, .neutral-btn.light:focus + background-color: rgba(255, 255, 255, 0.2) + border: 2px solid rgba(255, 255, 255, 0.8) + text-shadow: 0 1px 0 $clr-brick + +.neutral-btn.turquoise + border-color: $clr-turquoise + color: $clr-turquoise + +.neutral-btn.turquoise:hover, .neutral-btn.turquoise:active, .neutral-btn.turquoise:focus + background-color: rgba(0, 0, 0, 0.1) + text-shadow: 0 1px 0 #fff diff --git a/app/assets/templates/test.nghaml b/app/assets/templates/test.nghaml new file mode 100644 index 0000000000..99fb61374e --- /dev/null +++ b/app/assets/templates/test.nghaml @@ -0,0 +1 @@ +Frogs diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index 102c237c95..3dee7962c4 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -4,7 +4,12 @@ module Api def managed @enterprises = Enterprise.ransack(params[:q]).result.managed_by(current_api_user) - respond_with(@enterprises) + render params[:template] || :bulk_index + end + + def accessible + @enterprises = Enterprise.ransack(params[:q]).result.accessible_by(current_api_user) + render params[:template] || :bulk_index end end end diff --git a/app/controllers/api/order_cycles_controller.rb b/app/controllers/api/order_cycles_controller.rb index 89c815ea3c..b4b3486778 100644 --- a/app/controllers/api/order_cycles_controller.rb +++ b/app/controllers/api/order_cycles_controller.rb @@ -2,9 +2,15 @@ module Api class OrderCyclesController < Spree::Api::BaseController respond_to :json def managed + authorize! :admin, OrderCycle + authorize! :read, OrderCycle @order_cycles = OrderCycle.ransack(params[:q]).result.managed_by(current_api_user) - render :bulk_index + render params[:template] || :bulk_index + end + + def accessible + @order_cycles = OrderCycle.ransack(params[:q]).result.accessible_by(current_api_user) + render params[:template] || :bulk_index end end end - \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2f58096e8..9e4f4d4832 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,8 +1,5 @@ class ApplicationController < ActionController::Base protect_from_forgery - - before_filter :load_data_for_menu - before_filter :load_data_for_sidebar before_filter :require_certified_hostname include EnterprisesHelper @@ -16,27 +13,6 @@ class ApplicationController < ActionController::Base end private - def load_data_for_menu - @cms_site = Cms::Site.where(:identifier => 'open-food-network').first - end - - # This is getting sloppy, since @all_distributors is also used for order cycle selection, - # which is not in the sidebar. I don't like having an application controller method that's - # coupled to several parts of the code. We might be able to solve this using cells: - # https://github.com/apotonick/cells - def load_data_for_sidebar - sidebar_distributors_limit = false - sidebar_suppliers_limit = false - - @order_cycles = OrderCycle.active - - @sidebar_suppliers = Enterprise.is_primary_producer.with_supplied_active_products_on_hand.limit(sidebar_suppliers_limit) - @total_suppliers = Enterprise.is_primary_producer.distinct_count - - @sidebar_distributors = Enterprise.active_distributors.by_name.limit(sidebar_distributors_limit) - @all_distributors = Enterprise.active_distributors - @total_distributors = Enterprise.is_distributor.distinct_count - end def require_distributor_chosen unless current_distributor diff --git a/app/controllers/darkswarm_controller.rb b/app/controllers/darkswarm_controller.rb deleted file mode 100644 index b1a7422e02..0000000000 --- a/app/controllers/darkswarm_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class DarkswarmController < ApplicationController - def index - - end -end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 9f07e1d064..43c5216a8a 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,5 +1,9 @@ class HomeController < BaseController - layout 'landing_page' + layout 'darkswarm' + + def index + @active_distributors ||= Enterprise.distributors_with_active_order_cycles + end def new_landing_page end @@ -9,7 +13,6 @@ class HomeController < BaseController def temp_landing_page @groups = EnterpriseGroup.on_front_page.by_position - render layout: false end end diff --git a/app/controllers/producers_controller.rb b/app/controllers/producers_controller.rb new file mode 100644 index 0000000000..615f74871c --- /dev/null +++ b/app/controllers/producers_controller.rb @@ -0,0 +1,4 @@ +class ProducersController < BaseController + def index + end +end diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb index be97bd604f..8f28645ab8 100644 --- a/app/controllers/shop/checkout_controller.rb +++ b/app/controllers/shop/checkout_controller.rb @@ -22,26 +22,41 @@ class Shop::CheckoutController < Spree::CheckoutController state_callback(:after) else flash[:error] = t(:payment_processing_failed) - clear_ship_address - render :edit + update_failed return end end - if @order.state == "complete" || @order.completed? flash.notice = t(:order_processed_successfully) - respond_with(@order, :location => order_path(@order)) + respond_to do |format| + format.html do + respond_with(@order, :location => order_path(@order)) + end + format.js do + render json: {path: order_path(@order)}, status: 200 + end + end else - clear_ship_address - render :edit + update_failed end else - clear_ship_address - render :edit + update_failed end end private + + def update_failed + clear_ship_address + respond_to do |format| + format.html do + render :edit + end + format.js do + render json: {errors: @order.errors, flash: flash.to_hash}.to_json, status: 400 + end + end + end # When we have a pickup Shipping Method, we clone the distributor address into ship_address before_save # We don't want this data in the form, so we clear it out diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb new file mode 100644 index 0000000000..e6555eb291 --- /dev/null +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -0,0 +1,13 @@ +module Spree + module Admin + class OverviewController < Spree::Admin::BaseController + def index + if current_spree_user.admin? || current_spree_user.enterprises.any?{ |e| e.is_distributor? } + redirect_to admin_orders_path + elsif current_spree_user.enterprises.any?{ |e| e.is_primary_producer? } + redirect_to bulk_edit_admin_products_path + end + end + end + end +end \ No newline at end of file diff --git a/app/controllers/spree/api/orders_controller_decorator.rb b/app/controllers/spree/api/orders_controller_decorator.rb index 4f3c5c8c77..ca1fc4a570 100644 --- a/app/controllers/spree/api/orders_controller_decorator.rb +++ b/app/controllers/spree/api/orders_controller_decorator.rb @@ -7,7 +7,9 @@ Spree::Api::OrdersController.class_eval do before_filter :authorize_read!, :except => [:managed] def managed - @orders = Spree::Order.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page]) + authorize! :admin, Spree::Order + authorize! :read, Spree::Order + @orders = Spree::Order.ransack(params[:q]).result.distributed_by_user(current_api_user).page(params[:page]).per(params[:per_page]) respond_with(@orders, default_template: :index) end -end \ No newline at end of file +end diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index 7175ded8c1..0dbecaa9fd 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -1,5 +1,8 @@ Spree::Api::ProductsController.class_eval do def managed + authorize! :admin, Spree::Product + authorize! :read, Spree::Product + @products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page]) respond_with(@products, default_template: :index) end diff --git a/app/controllers/spree/api/variants_controller_decorator.rb b/app/controllers/spree/api/variants_controller_decorator.rb new file mode 100644 index 0000000000..a225c4c401 --- /dev/null +++ b/app/controllers/spree/api/variants_controller_decorator.rb @@ -0,0 +1,13 @@ +Spree::Api::VariantsController.class_eval do + def soft_delete + @variant = scope.find(params[:variant_id]) + authorize! :delete, @variant + + @variant.deleted_at = Time.now() + if @variant.save + respond_with(@variant, :status => 204) + else + invalid_resource!(@variant) + end + end +end diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index b21855bf7a..32e25bdfc5 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -10,4 +10,17 @@ module CheckoutHelper adjustments end + + def validated_input(name, path, args = {}) + attributes = { + required: true, + type: :text, + name: path, + id: path, + "ng-model" => path, + "ng-class" => "{error: !fieldValid('#{path}')}" + }.merge args + + render partial: "shared/validated_input", locals: {name: name, path: path, attributes: attributes} + end end diff --git a/app/helpers/spree/orders_helper.rb b/app/helpers/spree/orders_helper.rb index 86f295ae52..b72fb43db5 100644 --- a/app/helpers/spree/orders_helper.rb +++ b/app/helpers/spree/orders_helper.rb @@ -20,7 +20,7 @@ module Spree end def cart_count - current_order.andand.line_items.count || 0 + current_order.andand.line_items.andand.count || 0 end end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index ca31f5bc8c..5656f048c3 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -30,6 +30,7 @@ class Enterprise < ActiveRecord::Base after_validation :geocode_address scope :by_name, order('name') + scope :visible, where(:visible => true) scope :is_primary_producer, where(:is_primary_producer => true) scope :is_distributor, where(:is_distributor => true) scope :supplying_variant_in, lambda { |variants| joins(:supplied_products => :variants_including_master).where('spree_variants.id IN (?)', variants).select('DISTINCT enterprises.*') } @@ -47,23 +48,26 @@ class Enterprise < ActiveRecord::Base scope :with_distributed_products_outer, joins('LEFT OUTER JOIN product_distributions ON product_distributions.distributor_id = enterprises.id'). joins('LEFT OUTER JOIN spree_products ON spree_products.id = product_distributions.product_id') - scope :with_order_cycles_outer, + scope :with_order_cycles_as_distributor_outer, joins("LEFT OUTER JOIN exchanges ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')"). joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)') + scope :with_order_cycles_outer, + joins("LEFT OUTER JOIN exchanges ON (exchanges.receiver_id = enterprises.id OR exchanges.sender_id = enterprises.id)"). + joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)') scope :with_order_cycles_and_exchange_variants_outer, - with_order_cycles_outer. + with_order_cycles_as_distributor_outer. joins('LEFT OUTER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)'). joins('LEFT OUTER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)') scope :active_distributors, lambda { - with_distributed_products_outer.with_order_cycles_outer. + with_distributed_products_outer.with_order_cycles_as_distributor_outer. where('(product_distributions.product_id IS NOT NULL AND spree_products.deleted_at IS NULL AND spree_products.available_on <= ? AND spree_products.count_on_hand > 0) OR (order_cycles.id IS NOT NULL AND order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?)', Time.now, Time.now, Time.now). select('DISTINCT enterprises.*') } scope :distributors_with_active_order_cycles, lambda { - with_order_cycles_outer. + with_order_cycles_as_distributor_outer. merge(OrderCycle.active). select('DISTINCT enterprises.*') } @@ -86,12 +90,24 @@ class Enterprise < ActiveRecord::Base end } + # Return enterprises that participate in order cycles that user coordinates, sends to or receives from + scope :accessible_by, lambda { |user| + if user.has_spree_role?('admin') + scoped + else + with_order_cycles_outer. + where('order_cycles.id IN (?)', OrderCycle.accessible_by(user)). + select('DISTINCT enterprises.*') + end + } + # Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554 def self.distinct_count count(distinct: true) end + def self.find_near(suburb) enterprises = [] @@ -123,6 +139,13 @@ class Enterprise < ActiveRecord::Base Spree::Variant.joins(:product => :product_distributions).where('product_distributions.distributor_id=?', self.id) end + # Return all taxons for all distributed products + def taxons + Spree::Product.in_distributor(self).map do |p| + p.taxons + end.flatten.uniq + end + private def initialize_country diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index 148c18d367..f41ad76c96 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -1,5 +1,10 @@ class EnterpriseFee < ActiveRecord::Base belongs_to :enterprise + has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees' + has_many :exchange_fees, dependent: :destroy + has_many :exchanges, through: :exchange_fees + + before_destroy { order_cycles.clear } calculated_adjustments diff --git a/app/models/exchange.rb b/app/models/exchange.rb index 122586ba96..18c3f81447 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -28,6 +28,19 @@ class Exchange < ActiveRecord::Base scope :with_product, lambda { |product| joins(:exchange_variants).where('exchange_variants.variant_id IN (?)', product.variants_including_master) } + scope :managed_by, lambda { |user| + if user.has_spree_role?('admin') + scoped + else + joins('LEFT JOIN enterprises senders ON senders.id = exchanges.sender_id'). + joins('LEFT JOIN enterprises receivers ON receivers.id = exchanges.receiver_id'). + joins('LEFT JOIN enterprise_roles sender_roles ON sender_roles.enterprise_id = senders.id'). + joins('LEFT JOIN enterprise_roles receiver_roles ON receiver_roles.enterprise_id = receivers.id'). + where('sender_roles.user_id = ? AND receiver_roles.user_id = ?', user.id, user.id) + end + } + + def clone!(new_order_cycle) exchange = self.dup exchange.order_cycle = new_order_cycle diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 317c092375..f2ad8fee3e 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -64,6 +64,11 @@ class OrderCycle < ActiveRecord::Base with_distributor(distributor).soonest_opening.first end + def self.first_closing_for(distributor) + with_distributor(distributor).soonest_closing.first + end + + def self.most_recently_closed_for(distributor) with_distributor(distributor).most_recently_closed.first end @@ -79,11 +84,13 @@ class OrderCycle < ActiveRecord::Base end def suppliers - self.exchanges.incoming.map(&:sender).uniq + enterprise_ids = self.exchanges.incoming.pluck :sender_id + Enterprise.where('enterprises.id IN (?)', enterprise_ids) end def distributors - self.exchanges.outgoing.map(&:receiver).uniq + enterprise_ids = self.exchanges.outgoing.pluck :receiver_id + Enterprise.where('enterprises.id IN (?)', enterprise_ids) end def variants diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 75c89d8664..ee83e85bf7 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -8,13 +8,19 @@ class AbilityDecorator # when searching for variants to add to the order can [:create, :search, :bulk_update], nil + can [:admin, :index], :overview + # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| + can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], Spree::Product do |product| user.enterprises.include? product.supplier end - can [:admin, :index, :read, :create, :edit, :update, :search, :destroy], Spree::Variant + can [:create], Spree::Variant + can [:admin, :index, :read, :edit, :update, :search, :destroy], Spree::Variant do |variant| + user.enterprises.include? variant.product.supplier + end + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image @@ -23,11 +29,12 @@ class AbilityDecorator # Enterprise User can only access orders that they are a distributor for can [:index, :create], Spree::Order - can [:admin, :read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| + can [:read, :update, :bulk_management, :fire, :resend], Spree::Order do |order| # We allow editing orders with a nil distributor as this state occurs # during the order creation process from the admin backend order.distributor.nil? || user.enterprises.include?(order.distributor) end + can [:admin], Spree::Order if user.admin? || user.enterprises.any?{ |e| e.is_distributor? } can [:admin, :create], Spree::LineItem can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 31f56da0d3..7e5d660fc6 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -10,8 +10,7 @@ Spree::Order.class_eval do belongs_to :cart validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? } - attr_accessible :order_cycle_id, :distributor_id, :ship_address_same_as_billing - attr_accessor :ship_address_same_as_billing + attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor @@ -65,12 +64,6 @@ Spree::Order.class_eval do where("state != ?", state) } - # Accessors - # - def ship_address_same_as_billing=(string_value) - @ship_address_same_as_billing = (string_value == "true") - end - # -- Methods def products_available_from_new_distribution diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 23f88b3f35..165c2d34f6 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,5 +1,5 @@ Spree::Variant.class_eval do - has_many :exchange_variants + has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants attr_accessible :unit_value, :unit_description diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 1cb9707ecf..3158f464eb 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -31,6 +31,9 @@   = f.check_box :is_distributor = f.label :is_distributor, 'Hub' +   + = f.check_box :visible + = f.label :visible, 'Visible in search?' .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food. Select 'Hub' if you want a shop-front. You can choose either or both."} %a What's this? diff --git a/app/views/admin/enterprises/index.html.erb b/app/views/admin/enterprises/index.html.erb index 4047dc8b0d..1a4b5492eb 100644 --- a/app/views/admin/enterprises/index.html.erb +++ b/app/views/admin/enterprises/index.html.erb @@ -14,6 +14,7 @@ + @@ -22,6 +23,7 @@ Name Role + Active? Description @@ -38,6 +40,7 @@

<% end %> + <%= enterprise.visible ? 'Visible' : 'Invisible' %> <%= enterprise.description %> diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml new file mode 100644 index 0000000000..4396fabcba --- /dev/null +++ b/app/views/admin/order_cycles/_row.html.haml @@ -0,0 +1,25 @@ +- order_cycle = order_cycle_form.object +- klass = "order-cycle-#{order_cycle.id} #{order_cycle_status_class order_cycle}" +%tr{class: klass} + %td= link_to order_cycle.name, main_app.edit_admin_order_cycle_path(order_cycle) + %td= order_cycle_form.text_field :orders_open_at, :class => 'datetimepicker', :value => order_cycle.orders_open_at + %td= order_cycle_form.text_field :orders_close_at, :class => 'datetimepicker', :value => order_cycle.orders_close_at + %td.suppliers + - order_cycle.suppliers.managed_by(spree_current_user).each do |s| + = s.name + %br/ + %td= order_cycle.coordinator.name + %td.distributors + - order_cycle.distributors.managed_by(spree_current_user).each do |d| + = d.name + %br/ + + %td.products + - variant_images = capture do + - order_cycle.variants.each do |v| + = image_tag(v.images.first.attachment.url(:mini)) if v.images.present? + %br/ + %span.with-tip{'data-powertip' => variant_images}= "#{order_cycle.variants.count} variants" + + %td.actions + = link_to '', main_app.clone_admin_order_cycle_path(order_cycle), class: 'clone-order-cycle icon-copy no-text' diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 328d56d5aa..c3f4bc0b8f 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -18,6 +18,7 @@ %col %col %col + %thead %tr %th Name @@ -28,31 +29,9 @@ %th Distributors %th Products %th.actions + %tbody = f.fields_for :collection do |order_cycle_form| - - order_cycle = order_cycle_form.object - - klass = "order-cycle-#{order_cycle.id} #{order_cycle_status_class order_cycle}" - %tr{class: klass} - %td= link_to order_cycle.name, main_app.edit_admin_order_cycle_path(order_cycle) - %td= order_cycle_form.text_field :orders_open_at, :class => 'datetimepicker', :value => order_cycle.orders_open_at - %td= order_cycle_form.text_field :orders_close_at, :class => 'datetimepicker', :value => order_cycle.orders_close_at - %td.suppliers - - order_cycle.suppliers.each do |s| - = s.name - %br/ - %td= order_cycle.coordinator.name - %td.distributors - - order_cycle.distributors.each do |d| - = d.name - %br/ + = render 'admin/order_cycles/row', order_cycle_form: order_cycle_form - %td.products - - variant_images = capture do - - order_cycle.variants.each do |v| - = image_tag(v.images.first.attachment.url(:mini)) if v.images.present? - %br/ - %span.with-tip{'data-powertip' => variant_images}= "#{order_cycle.variants.count} variants" - - %td.actions - = link_to '', main_app.clone_admin_order_cycle_path(order_cycle), class: 'clone-order-cycle icon-copy no-text' = f.submit 'Update' diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep index 1f91cbe3e9..04fc659813 100644 --- a/app/views/admin/order_cycles/show.rep +++ b/app/views/admin/order_cycles/show.rep @@ -9,7 +9,7 @@ r.element :order_cycle, @order_cycle do r.element :id end - r.list_of :exchanges, @order_cycle.exchanges.order('id ASC') do |exchange| + r.list_of :exchanges, @order_cycle.exchanges.managed_by(spree_current_user).order('id ASC') do |exchange| r.element :id r.element :sender_id r.element :receiver_id diff --git a/app/views/darkswarm/index.html.haml b/app/views/darkswarm/index.html.haml deleted file mode 100644 index 5938185c06..0000000000 --- a/app/views/darkswarm/index.html.haml +++ /dev/null @@ -1 +0,0 @@ -TESTING diff --git a/app/views/home/_beta.html.haml b/app/views/home/_beta.html.haml new file mode 100644 index 0000000000..318608edb4 --- /dev/null +++ b/app/views/home/_beta.html.haml @@ -0,0 +1,13 @@ +#beta + + .row + .small-12.columns.text-center + %h2 S'cuse us + %h5 while we get (more) awesome + %p Open Food Network (beta) is a new service that’s being built right now! Our food producers are currently based around Melbourne and Victoria, and we hope to expand OFN nationally very soon. + %p Want to help? Or find out when OFN is coming to you? + %strong We’d love to hear from you: + %p + %a{title:'Food buyers', href: '/food-buyers'} Food buyers + | + %a{title:'Food producers & farmers', href: '/food-producers'} Food producers & farmers \ No newline at end of file diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml new file mode 100644 index 0000000000..7a0af31130 --- /dev/null +++ b/app/views/home/_fat.html.haml @@ -0,0 +1,28 @@ +.row.active_table_row{"ng-show" => "open()"} + .columns.small-4 + %strong Shop for + %p.trans-sentence + {{ hub.taxons | printArray }} + .columns.small-4 + %strong Delivery options + %ol + %li.pickup{"bo-if" => "hub.pickup"} Pickup + %li.delivery{"bo-if" => "hub.delivery"} Delivery + .columns.small-4 + %strong Our producers + %p + Go to our shop to see our current producers + +.row.active_table_row.link{"ng-show" => "open()", "ng-if" => "hub.active"} + .columns.small-11 + %a{"bo-href" => "hub.path", "ng-show" => "!emptiesCart()"} + Shop at + + %strong {{ hub.name }} + %a{"ng-click" => "changeHub()", "ng-show" => "emptiesCart()"} + Change hub to + %strong {{ hub.name }} + + .columns.small-1.text-right + %a{"bo-href" => "hub.path"} + %i.fi-arrow-right diff --git a/app/views/home/_groups.html.haml b/app/views/home/_groups.html.haml new file mode 100644 index 0000000000..5f651e3728 --- /dev/null +++ b/app/views/home/_groups.html.haml @@ -0,0 +1,10 @@ +#groups + + .row + .small-12.columns.text-center + %h2 Groups / Regions + %h5 See all the groups & regions on the Open Food Network + %p + %button.neutral-btn.light + %i.fi-torsos-all + View groups & regions \ No newline at end of file diff --git a/app/views/home/_hubs.html.haml b/app/views/home/_hubs.html.haml new file mode 100644 index 0000000000..a1d821e721 --- /dev/null +++ b/app/views/home/_hubs.html.haml @@ -0,0 +1,36 @@ +#hubs{"ng-controller" => "HubsCtrl"} + :javascript + angular.module('Darkswarm').value('hubs', #{render "json/hubs"}) + + .row + .small-12.columns.text-center + %h1 Ready to shop? + %div + Select a + %ofn-modal{title: "food hub"} + = render partial: "modals/food_hub" + from the list below: + %p + + #hub-search.row + .small-12.columns + %input{type: :text, + "ng-model" => "query", + placeholder: "Search postcode, suburb or hub name...", + "ng-debounce" => "150", + "ofn-disable-enter" => true} + + .row{bindonce: true} + .small-12.columns + .active_table + %hub.active_table_node.row{"ng-repeat" => "hub in filteredHubs = (hubs | filterHubs:query)", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "HubNodeCtrl", + id: "{{hub.path}}"} + .small-12.columns + = render partial: 'home/skinny' + = render partial: 'home/fat' + + .row{"ng-show" => "filteredHubs.length == 0"} + .columns.small-12.text-center + No results diff --git a/app/views/home/_map.html.haml b/app/views/home/_map.html.haml new file mode 100644 index 0000000000..b9898806aa --- /dev/null +++ b/app/views/home/_map.html.haml @@ -0,0 +1,10 @@ +#map + + .row + .small-12.columns.text-center + %h2 Map + %h5 of all our food hubs and producers + %p + %button.neutral-btn.light + %i.fi-map + View map \ No newline at end of file diff --git a/app/views/home/_producers.html.haml b/app/views/home/_producers.html.haml new file mode 100644 index 0000000000..7dca6143e2 --- /dev/null +++ b/app/views/home/_producers.html.haml @@ -0,0 +1,10 @@ +#producers + + .row + .small-12.columns.text-center + %h2 Producers + %h5 Looking for a specific producer or farmer? + %p + %button.neutral-btn.turquoise + %i.fi-trees + View all producers \ No newline at end of file diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml new file mode 100644 index 0000000000..fcf860575e --- /dev/null +++ b/app/views/home/_skinny.html.haml @@ -0,0 +1,13 @@ +.row.active_table_row{"ng-click" => "toggle()", "ng-class" => "{'closed' : !open()}"} + .columns.small-4 + %strong {{ hub.name }} + .columns.small-3 + {{ hub.address.city }} + .columns.small-1 + {{ hub.address.state | uppercase }} + .columns.small-3{"bo-if" => "hub.active"} + {{ hub.orders_close_at | sensible_timeframe }} + .columns.small-3{"bo-if" => "!hub.active"} + Orders closed + .columns.small-1.text-right + %i{"ng-class" => "{'fi-arrow-down' : !open(), 'fi-arrow-up' : open()}"} diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml new file mode 100644 index 0000000000..b441ac647f --- /dev/null +++ b/app/views/home/index.html.haml @@ -0,0 +1,20 @@ +#tagline + .row + .small-12.text-center.columns + %h1= image_tag "ofn_logo_beta.png", title: "Open Food Network (beta)" + %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. + + %ofn-modal{title: "Learn more"} + = render partial: "modals/learn_more" + += render partial: "home/hubs" + += render partial: "home/map" + += render partial: "home/producers" + += render partial: "home/groups" + += render partial: "home/beta" + += render partial: "shared/footer" diff --git a/app/views/json/_current_hub.rabl b/app/views/json/_current_hub.rabl new file mode 100644 index 0000000000..ab67cce3cc --- /dev/null +++ b/app/views/json/_current_hub.rabl @@ -0,0 +1,2 @@ +object current_distributor +attributes :name, :id diff --git a/app/views/json/_current_user.rabl b/app/views/json/_current_user.rabl new file mode 100644 index 0000000000..b07ae07b66 --- /dev/null +++ b/app/views/json/_current_user.rabl @@ -0,0 +1,2 @@ +object spree_current_user +attributes :email, :id diff --git a/app/views/json/_hubs.rabl b/app/views/json/_hubs.rabl new file mode 100644 index 0000000000..dbd0538caf --- /dev/null +++ b/app/views/json/_hubs.rabl @@ -0,0 +1,33 @@ +collection Enterprise.is_distributor +attributes :name, :id + +child :taxons do + attributes :name, :id +end + +child :address do + attributes :city, :zipcode + node :state do |address| + address.state.abbr + end +end + +node :pickup do |hub| + not hub.shipping_methods.where(:require_ship_address => false).empty? +end + +node :delivery do |hub| + not hub.shipping_methods.where(:require_ship_address => true).empty? +end + +node :path do |hub| + shop_enterprise_path(hub) +end + +node :active do |hub| + @active_distributors.include?(hub) +end + +node :orders_close_at do |hub| + OrderCycle.first_closing_for(hub).andand.orders_close_at +end diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 51aaca837d..e44c0600af 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -9,12 +9,16 @@ = stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" + = render "layouts/bugherd_script" = csrf_meta_tags %body.off-canvas{"ng-app" => "Darkswarm"} + = render partial: "shared/current_hub" + = render partial: "shared/current_user" = render partial: "shared/menu" = display_flash_messages + %ofn-flash = render "shared/sidebar" diff --git a/app/views/layouts/landing_page.html.haml b/app/views/layouts/landing_page.html.haml index 893c112899..e03086a2b8 100644 --- a/app/views/layouts/landing_page.html.haml +++ b/app/views/layouts/landing_page.html.haml @@ -37,13 +37,16 @@ %li= link_to "Distributors", "#", :data => { "reveal-id" => "become-distributor" } %li.divider %li= link_to "Farmers", "#", :data => { "reveal-id" => "become-farmer" } + %section{ role: "main" } = yield + %section#sidebar{ role: "complementary" } .login-panel #login-content.hide = render "home/login" #sign-up-content.hide = render "home/signup" + = yield :scripts diff --git a/app/views/modals/_food_hub.html.haml b/app/views/modals/_food_hub.html.haml new file mode 100644 index 0000000000..ceba6192f3 --- /dev/null +++ b/app/views/modals/_food_hub.html.haml @@ -0,0 +1,4 @@ +%h2 Food Hubs +%h5 Our food hubs are the point of contact between you and the people who make your food! +%p You can search for a convenient hub by location or name. Some hubs have multiple points where you can pick-up your purchases, and some will also provide delivery options. Each food hub is a sales point with independent business operations and logisitics - so variations between hubs are to be expected. +%a.close-reveal-modal{"ng-click" => "cancel()"} × \ No newline at end of file diff --git a/app/views/modals/_learn_more.html.haml b/app/views/modals/_learn_more.html.haml new file mode 100644 index 0000000000..1cfdaf6c00 --- /dev/null +++ b/app/views/modals/_learn_more.html.haml @@ -0,0 +1,9 @@ +%h2 How it works +%h5 Shop the Open Food Network +%p Search for a food hub near you to start shopping! You can expand each food hub to see what kinds of goodies are available, and click through to start shopping. +%h5 Pick-ups, delivery & shipping costs +%p Some food hubs deliver to your door, while others require you to pick-up your purchases. You can see which options are available on the homepage, and select which you'd like at the shopping and check-out pages. Delivery will cost more, and pricing differs from hub-to-hub. Each food hub is a sales point with independent business operations and logisitics - so variations between hubs are to be expected. +%h5 Learn more +%p If you want to learn more about the Open Food Network, how it works, and get involved, check out: +%a.button.neutral-btn.dark{:href => "http://www.openfoodnetwork.org" , :target => "_blank" } Open Food Network +%a.close-reveal-modal{"ng-click" => "cancel()"} × diff --git a/app/views/producers/index.haml b/app/views/producers/index.haml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/shared/_current_hub.haml b/app/views/shared/_current_hub.haml new file mode 100644 index 0000000000..5a0ff7c564 --- /dev/null +++ b/app/views/shared/_current_hub.haml @@ -0,0 +1,2 @@ +:javascript + angular.module('Darkswarm').value('currentHub', #{render "json/current_hub"}) diff --git a/app/views/shared/_current_user.haml b/app/views/shared/_current_user.haml new file mode 100644 index 0000000000..9745a71313 --- /dev/null +++ b/app/views/shared/_current_user.haml @@ -0,0 +1,2 @@ +:javascript + angular.module('Darkswarm').value('user', #{render "json/current_user"}) diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 4b2ae59124..eb655b7c37 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -1,6 +1,27 @@ %footer .row.landing-page-row - .large-6.columns.text-left - %strong - %span.has-tip.tip-top{"data-tooltip" => "", "data-options" => "disable-for-touch:true", title: "Open Food Network is a marketplace connecting you to local producers and food hubs"} - = link_to "WHAT IS OPEN FOOD NETWORK?", "http://openfoodnetwork.org" + .small-5.columns.text-left + %h3 Follow us + %p + %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/OpenFoodNet', target: '_blank'} + %i.fi-social-facebook + Facebook    + %a{title:'Follow us on Twitter', href: 'https://twitter.com/OpenFoodNet', target: '_blank'} + %i.fi-social-twitter + Twitter    + %a{title:'Join our group on LinkedIn', href: 'http://www.linkedin.com/groups/Open-Food-Foundation-4743336', target: '_blank'} + %i.fi-social-linkedin + LinkedIn    + %p + %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} + %span.email + = "hello@openfoodnetwork.org".reverse + .small-2.columns + .small-5.columns.text-right + %h3 About us + %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. + .row.landing-page-row + .small-12.columns.text-center + %p + %p © Copyright 2014 Open Food Foundation + %small Content | Terms of Service | Code - Opensource on Github | Creative Commons / IP | OFN.org diff --git a/app/views/shared/_menu.html.haml b/app/views/shared/_menu.html.haml index f5aee8cc17..dcd3897195 100644 --- a/app/views/shared/_menu.html.haml +++ b/app/views/shared/_menu.html.haml @@ -2,8 +2,9 @@ %section.top-bar-section %ul.left{"ng-controller" => "AuthenticationActionsCtrl"} %li - %a.icon{"ng-click" => "toggle()"} + %a.icon{"ng-click" => "Sidebar.toggle()"} %i.fi-list + %li= link_to image_tag("ofn_logo_small.png"), root_path %li.divider - if spree_current_user.nil? @@ -13,6 +14,9 @@ %section.top-bar-section %ul.right + %li.current_hub{"ng-controller" => "CurrentHubCtrl", "ng-show" => "CurrentHub.id"} + %a{href: main_app.shop_path} + {{ CurrentHub.name }} %li.cart %a.icon{href: cart_url} %i.fi-shopping-cart diff --git a/app/views/shared/_sidebar.html.haml b/app/views/shared/_sidebar.html.haml index 04891a5c82..158083d584 100644 --- a/app/views/shared/_sidebar.html.haml +++ b/app/views/shared/_sidebar.html.haml @@ -1,5 +1,5 @@ %section#sidebar{ role: "complementary", "ng-controller" => "SidebarCtrl", -"ng-class" => "{'active' : active()}"} +"ng-class" => "{'active' : Sidebar.active()}"} - if spree_current_user.nil? %tabset diff --git a/app/views/shared/_signed_out.html.haml b/app/views/shared/_signed_out.html.haml index b42c104769..1ea02cc4ea 100644 --- a/app/views/shared/_signed_out.html.haml +++ b/app/views/shared/_signed_out.html.haml @@ -1,5 +1,5 @@ -%li#login-link - %a.sidebar-button{"ng-click" => "toggle('/login')"} Login -%li.divider -%li#sign-up-link - %a.sidebar-button{"ng-click" => "toggle('/signup')"} Sign Up +-#%li#login-link + -#%a.sidebar-button{"ng-click" => "toggle('/login')"} Login +-#%li.divider +-#%li#sign-up-link + -#%a.sidebar-button{"ng-click" => "toggle('/signup')"} Sign Up diff --git a/app/views/shared/_validated_input.html.haml b/app/views/shared/_validated_input.html.haml new file mode 100644 index 0000000000..46331333fc --- /dev/null +++ b/app/views/shared/_validated_input.html.haml @@ -0,0 +1,6 @@ +%label{for: path}= name + +%input.medium.input-text{attributes} + +%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"} + = "{{ fieldErrors('#{path}') }}" diff --git a/app/views/shop/checkout/_authentication.html.haml b/app/views/shop/checkout/_authentication.html.haml index 62aba59509..891958de25 100644 --- a/app/views/shop/checkout/_authentication.html.haml +++ b/app/views/shop/checkout/_authentication.html.haml @@ -1,9 +1,9 @@ %fieldset - %accordion-group{heading: "User", "is-open" => "userOpen"} + %accordion-group{heading: "User", "is-open" => "accordion.user"} .row .large-4.columns.text-center{"ng-controller" => "AuthenticationActionsCtrl"} %button{"ng-click" => "toggle('/login')"} Login .large-4.columns.text-center{"ng-controller" => "AuthenticationActionsCtrl"} %button{"ng-click" => "toggle('/signup')"} Signup .large-4.columns.text-center - %button{"ng-click" => "scrollTo('details')"} Checkout as guest + %button{"ng-click" => "show('details')"} Checkout as guest diff --git a/app/views/shop/checkout/_billing.html.haml b/app/views/shop/checkout/_billing.html.haml new file mode 100644 index 0000000000..6872bcaabc --- /dev/null +++ b/app/views/shop/checkout/_billing.html.haml @@ -0,0 +1,38 @@ +%fieldset#billing + %ng-form{"ng-controller" => "BillingCtrl", name: "billing"} + %accordion-group{"is-open" => "accordion.billing", + "ng-class" => "{valid: billing.$valid}"} + %accordion-heading + .row + .large-6.columns + Billing + %i.fi-x + %i.fi-check + .large-6.columns.text-right + {{ order.bill_address.address1 }} + {{ order.bill_address.city }} + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .large-12.columns + = validated_input "Address", "order.bill_address.address1", "ofn-focus" => "accordion['billing']" + .row + .large-12.columns + = validated_input "Address (contd.)", "order.bill_address.address2", required: false + .row + .large-6.columns + = validated_input "City", "order.bill_address.city" + + .large-6.columns + = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, {include_blank: false}, + "ng-model" => "order.bill_address.state_id" + .row + .large-6.columns + = validated_input "Postcode", "order.bill_address.zipcode" + + .large-6.columns.right + = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, + {include_blank: false}, "ng-model" => "order.bill_address.country_id" + + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/shop/checkout/_details.html.haml b/app/views/shop/checkout/_details.html.haml new file mode 100644 index 0000000000..8b626f924f --- /dev/null +++ b/app/views/shop/checkout/_details.html.haml @@ -0,0 +1,32 @@ +%fieldset#details + %ng-form{"ng-controller" => "DetailsCtrl", name: "details"} + %accordion-group{"is-open" => "accordion.details", + "ng-class" => "{valid: details.$valid}"} + %accordion-heading + .row + .large-6.columns + Customer Details + %i.fi-x + %i.fi-check + .large-6.columns.text-right + {{ order.bill_address.firstname }} + {{ order.bill_address.lastname }} + .row + .large-6.columns + = validated_input 'Email', 'order.email', type: :email, "ofn-focus" => "accordion['details']" + + = f.fields_for :bill_address, @order.bill_address do |ba| + .large-6.columns + = validated_input 'Phone', 'order.bill_address.phone' + + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .large-6.columns + = validated_input "First Name", "order.bill_address.firstname" + + .large-6.columns + = validated_input "Last Name", "order.bill_address.lastname" + + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} Next diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml index cd3b277dbb..a198213397 100644 --- a/app/views/shop/checkout/_form.html.haml +++ b/app/views/shop/checkout/_form.html.haml @@ -1,143 +1,17 @@ -%checkout{"ng-controller" => "CheckoutCtrl"} += f_form_for current_order, url: main_app.shop_update_checkout_path, + html: {name: "checkout", + id: "checkout_form", + novalidate: true, + name: "checkout"} do |f| - = f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form"} do |f| + :javascript + angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) - :javascript - angular.module('Darkswarm').value('order', #{render "shop/checkout/order"}) - - -#%pre - -#{{ order | json }} - - .large-12.columns - %fieldset#details{name: "details"} - %legend Customer Details - .row - .large-6.columns - = f.text_field :email - = f.fields_for :bill_address, @order.bill_address do |ba| - .large-6.columns - = ba.text_field :phone, "ng-model" => "order.bill_address.phone" - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-6.columns - = ba.text_field :firstname, "ng-model" => "order.bill_address.firstname" - .large-6.columns - = ba.text_field :lastname, "ng-model" => "order.bill_address.lastname" - - %fieldset#billing - %legend Billing Address - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .large-12.columns - = ba.text_field :address1, - "ng-model" => "order.bill_address.address1" - .row - .large-12.columns - = ba.text_field :address2, - "ng-model" => "order.bill_address.address2" - .row - .large-6.columns - - = ba.text_field :city, - "ng-model" => "order.bill_address.city" - - .large-6.columns - = ba.select :state_id, @order.billing_address.country.states.map{|c|[c.name, c.id]}, - "ng-model" => "order.bill_address.state_id" - .row - .large-6.columns - = ba.text_field :zipcode, label: "Postcode", - "ng-model" => "order.bill_address.zipcode" - .large-6.columns.right - = ba.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false}, "ng-model" => "order.bill_address.country_id" - - %fieldset#shipping - %legend Shipping - - for ship_method, i in current_distributor.shipping_methods.uniq - .row - .large-12.columns - -#= f.radio_button :shipping_method_id, ship_method.id, - -#text: ship_method.name, - -#"ng-change" => "shippingMethodChanged()", - -#"ng-model" => "order.shipping_method_id" - %label - = radio_button_tag "order[shipping_method_id]", ship_method.id, false, - "ng-change" => "shippingMethodChanged()", - "ng-model" => "order.shipping_method_id" - = ship_method.name - - #distributor_address.panel{"ng-show" => "!require_ship_address"} - = @order.distributor.distributor_info.andand.html_safe - = @order.order_cycle.pickup_time_for(@order.distributor) - = @order.order_cycle.pickup_instructions_for(@order.distributor) - - = f.fields_for :ship_address, @order.ship_address do |sa| - - #ship_address{"ng-show" => "require_ship_address"} - %label - = hidden_field_tag "order[ship_address_same_as_billing]", "false" - = check_box_tag "order[ship_address_same_as_billing]", true, @order.ship_address_same_as_billing, - "ng-model" => "order.ship_address_same_as_billing" - Shipping address same as billing address? - - %div.visible{"ng-show" => "!order.ship_address_same_as_billing"} - .row - .large-12.columns - = sa.text_field :address1 - .row - - .large-12.columns - = sa.text_field :address2 - - .row - .large-6.columns - = sa.text_field :city - .large-6.columns - = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} - .row - .large-6.columns - = sa.text_field :zipcode, label: "Postcode" - .large-6.columns.right - = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, - {include_blank: false} - .row - .large-6.columns - = sa.text_field :firstname - .large-6.columns - = sa.text_field :lastname - .row - .large-6.columns - = sa.text_field :phone - - #ship_address_hidden{"ng-show" => "order.ship_address_same_as_billing"} - = sa.hidden_field :address1, "ng-value" => "order.bill_address.address1", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :address2, "ng-value" => "order.bill_address.address2", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :city, "ng-value" => "order.bill_address.city", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :country_id, "ng-value" => "order.bill_address.country_id", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :zipcode, "ng-value" => "order.bill_address.zipcode", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :firstname, "ng-value" => "order.bill_address.firstname", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :lastname, "ng-value" => "order.bill_address.lastname", - "ng-disabled" => "!order.ship_address_same_as_billing" - = sa.hidden_field :phone, "ng-value" => "order.bill_address.phone", - "ng-disabled" => "!order.ship_address_same_as_billing" - - %fieldset#payment - %legend Payment Details - - current_order.available_payment_methods.each do |method| - .row - .large-12.columns - %label - = radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false, - "ng-model" => "order.payment_method_id" - = method.name - .row{"ng-show" => "order.payment_method_id == #{method.id}"} - .large-12.columns - = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } + -#%pre + -#{{ Order.order == order }} + .large-12.columns + = render partial: "shop/checkout/details", locals: {f: f} + = render partial: "shop/checkout/billing", locals: {f: f} + = render partial: "shop/checkout/shipping", locals: {f: f} + = render partial: "shop/checkout/payment", locals: {f: f} diff --git a/app/views/shop/checkout/_login.html.haml b/app/views/shop/checkout/_login.html.haml deleted file mode 100644 index 33bf41fed5..0000000000 --- a/app/views/shop/checkout/_login.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= form_for Spree::User.new, :html => {'data-type' => :json}, :as => :spree_user, :url => spree.spree_user_session_path do |f| - %fieldset - %legend I have an OFN Account - %p - = f.label :email, t(:email) - = f.email_field :email, :class => 'title', :tabindex => 1, :id => "login_spree_user_email" - %p - = f.label :password, t(:password) - = f.password_field :password, :class => 'title', :tabindex => 2, :id => "login_spree_user_password" - %p - %label - = f.check_box :remember_me - = f.label :remember_me, t(:remember_me) - %p= f.submit t(:login), :class => 'button primary', :tabindex => 3, :id => "login_spree_user_remember_me" diff --git a/app/views/shop/checkout/_order.rabl b/app/views/shop/checkout/_order.rabl index 09451830fb..27ad32c6e3 100644 --- a/app/views/shop/checkout/_order.rabl +++ b/app/views/shop/checkout/_order.rabl @@ -1,5 +1,5 @@ object current_order -attributes :id, :email, :shipping_method_id, :ship_address_same_as_billing +attributes :id, :email, :shipping_method_id node :display_total do current_order.display_total.money.to_f @@ -17,12 +17,20 @@ child current_order.ship_address => :ship_address do attributes :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, :state_id, :zipcode end -# Format here is {id: require_ship_address} node :shipping_methods do Hash[current_order.distributor.shipping_methods.collect { |method| [method.id, { require_ship_address: method.require_ship_address, - price: method.compute_amount(current_order).to_f + price: method.compute_amount(current_order).to_f, + name: method.name + }] + }] +end + +node :payment_methods do + Hash[current_order.available_payment_methods.collect { + |method| [method.id, { + name: method.name }] }] end diff --git a/app/views/shop/checkout/_payment.html.haml b/app/views/shop/checkout/_payment.html.haml new file mode 100644 index 0000000000..6762924d36 --- /dev/null +++ b/app/views/shop/checkout/_payment.html.haml @@ -0,0 +1,23 @@ +%fieldset#payment + %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} + %accordion-group{"is-open" => "accordion.payment", + "ng-class" => "{valid: payment.$valid}"} + %accordion-heading + .row + .large-6.columns + Payment Details + %i.fi-x + %i.fi-check + .large-6.columns.text-right + {{ Order.paymentMethod().name }} + - current_order.available_payment_methods.each do |method| + .row + .large-12.columns + %label + = radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false, + "ng-model" => "order.payment_method_id" + = method.name + .row{"ng-show" => "order.payment_method_id == #{method.id}"} + .large-12.columns + = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } + diff --git a/app/views/shop/checkout/_shipping.html.haml b/app/views/shop/checkout/_shipping.html.haml new file mode 100644 index 0000000000..d70b717525 --- /dev/null +++ b/app/views/shop/checkout/_shipping.html.haml @@ -0,0 +1,62 @@ +%fieldset#shipping + %ng-form{"ng-controller" => "ShippingCtrl", name: "shipping"} + %accordion-group{"is-open" => "accordion.shipping", + "ng-class" => "{valid: shipping.$valid}"} + %accordion-heading + .row + .large-6.columns + Shipping + %i.fi-x + %i.fi-check + .large-6.columns.text-right + {{ Order.shippingMethod().name }} + - for ship_method, i in current_distributor.shipping_methods.uniq + .row + .large-12.columns + %label + -#= radio_button_tag "order[shipping_method_id]", ship_method.id, false, + -#"ng-model" => "order.shipping_method_id" + %input{type: :radio, value: ship_method.id, + "ng-model" => "order.shipping_method_id"} + = ship_method.name + + #distributor_address.panel{"ng-show" => "!Order.requireShipAddress()"} + = @order.distributor.distributor_info.andand.html_safe + = @order.order_cycle.pickup_time_for(@order.distributor) + = @order.order_cycle.pickup_instructions_for(@order.distributor) + + = f.fields_for :ship_address, @order.ship_address do |sa| + #ship_address{"ng-if" => "Order.requireShipAddress()"} + %label + %input{type: :checkbox, "ng-model" => "CheckoutFormState.ship_address_same_as_billing"} + Shipping address same as billing address? + + %div.visible{"ng-if" => "!CheckoutFormState.ship_address_same_as_billing"} + .row + .large-12.columns + = validated_input "Address", "order.ship_address.address1", "ofn-focus" => "accordion['shipping']" + .row + .large-12.columns + = validated_input "Address (contd.)", "order.ship_address.address2", required: false + .row + .large-6.columns + = validated_input "City", "order.ship_address.city" + .large-6.columns + = sa.select :state_id, @order.shipping_address.country.states.map{|c|[c.name, c.id]} + .row + .large-6.columns + = validated_input "Postcode", "order.ship_address.zipcode" + .large-6.columns.right + = sa.select :country_id, available_countries.map{|c|[c.name, c.id]}, + {include_blank: false} + .row + .large-6.columns + = validated_input "First Name", "order.ship_address.firstname" + .large-6.columns + = validated_input "Last Name", "order.ship_address.lastname" + .row + .large-6.columns + = validated_input "Phone", "order.ship_address.phone" + .row + .large-12.columns.text-right + %button{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)", "ofn-focus" => "accordion['shipping']"} Next diff --git a/app/views/shop/checkout/_signup.html.haml b/app/views/shop/checkout/_signup.html.haml deleted file mode 100644 index 81ed7744d0..0000000000 --- a/app/views/shop/checkout/_signup.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= form_for Spree::User.new, :as => :spree_user, :url => spree.spree_user_registration_path(@spree_user) do |f| - %fieldset - %legend New to OFN? - %p - = f.label :email, t(:email) - = f.email_field :email, :class => 'title', :id => "signup_spree_user_email" - %p - = f.label :password, t(:password) - = f.password_field :password, :class => 'title', :id => "signup_spree_user_password" - %p - = f.label :password_confirmation, t(:confirm_password) - = f.password_field :password_confirmation, :class => 'title', :id => "signup_spree_user_password_confirmation" - - = f.submit "Sign Up", :class => 'button' diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml index 8acd254d69..c2a0a34270 100644 --- a/app/views/shop/checkout/_summary.html.haml +++ b/app/views/shop/checkout/_summary.html.haml @@ -1,4 +1,4 @@ -%orderdetails{"ng-controller" => "CheckoutCtrl"} +%orderdetails = form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f| %fieldset %legend Your Order @@ -13,15 +13,16 @@ %td= adjustment.display_amount.to_html %tr %th Shipping - %td {{ shippingPrice() | currency }} + %td {{ Order.shippingPrice() | currency }} %tr %th Cart total - %td {{ cartTotal() | currency }} + %td {{ Order.cartTotal() | currency }} - if current_order.price_adjustment_totals.present? - current_order.price_adjustment_totals.each do |label, total| %tr %th= label %td= total - = f.submit "Purchase", class: "button" + = f.submit "Purchase", class: "button", "ng-disabled" => "checkout.$invalid", "ofn-focus" => "accordion['payment']" %a.button.secondary{href: cart_url} Back to Cart + diff --git a/app/views/shop/shop/_order_cycles.html.haml b/app/views/shop/shop/_order_cycles.html.haml index d121bb5fb8..3226771a14 100644 --- a/app/views/shop/shop/_order_cycles.html.haml +++ b/app/views/shop/shop/_order_cycles.html.haml @@ -2,7 +2,7 @@ :javascript angular.module('Darkswarm').value('orderCycleData', #{render "shop/shop/order_cycle"}) - - if @order_cycles.empty? + - if @order_cycle and @order_cycles.empty? Orders are currently closed for this hub %p Please contact your hub directly to see if they accept late orders, @@ -14,3 +14,5 @@ - else %form.custom = yield :order_cycle_form + + diff --git a/app/views/shop/shop/_products.html.haml b/app/views/shop/shop/_products.html.haml index f8a2cbdbf7..57f03632fa 100644 --- a/app/views/shop/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -1,7 +1,12 @@ -%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null"} +%products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", +"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} + = form_for :order, :url => populate_orders_path, html: {:class => "custom"} do - %input#search.text{"ng-model" => "query", placeholder: "Search", "ng-keypress" => "searchKeypress($event)"} + %input#search.text{"ng-model" => "query", + placeholder: "Search", + "ng-debounce" => "150", + "ng-keypress" => "searchKeypress($event)"} %input.button.right{type: :submit, value: "Add to Cart"} %table @@ -16,17 +21,20 @@ %tr %td{colspan: 6} %h3.text-center Loading Products - %tbody{"ng-repeat" => "product in data.products | filter:query"} + %tbody{"ng-repeat" => "product in data.products | filter:query | limitTo: limit track by product.id"} %tr{"class" => "product product-{{ product.id }}"} - %td.name - %img{"ng-src" => "{{ product.master.images[0].small_url }}"} + + %td.name{bindonce: "product"} + %img{"bo-src" => "product.master.images[0].small_url"} %div %h5 {{ product.name }} %a{"data-reveal-id" => "producer_details_{{product.supplier.id}}", "data-reveal" => ""} {{ product.supplier.name }} - %td.notes {{ product.notes | truncate:80 }} - %td + + %td.notes{bindonce: ""} {{ product.notes | truncate:80 }} + + %td{bindonce: ""} %span{"ng-hide" => "product.variants.length > 0"} {{ product.master.options_text }} %span{"ng-show" => "product.variants.length > 0"} %img.collapse{src: "/assets/collapse.png", @@ -41,24 +49,31 @@ %input{type: :number, value: nil, min: 0, + "ofn-disable-scroll" => true, max: "{{product.on_demand && 9999 || product.count_on_hand }}", name: "variants[{{product.master.id}}]", id: "variants_{{product.master.id}}", "ng-model" => "product.quantity"} + %td.group_buy %span{"ng-show" => "product.group_buy && (product.variants.length == 0)"} %input{type: :number, min: 0, + "ofn-disable-scroll" => true, max: "{{product.on_demand && 9999 || product.count_on_hand }}", name: "variant_attributes[{{product.master.id}}][max_quantity]", "ng-model" => "product.max_quantity"} - %td.price.text-right + + %td.price.text-right{bindonce: ""} %small{"ng-show" => "(product.variants.length > 0)"} from {{ productPrice(product) | currency }} - %tr.product-description + + %tr.product-description{bindonce: ""} %td{colspan: 2}{{ product.notes | truncate:80 }} - %tr.variant{"ng-repeat" => "variant in product.variants", "ng-show" => "product.show_variants"} + + %tr.variant{"ng-repeat" => "variant in product.variants", "ng-if" => "product.show_variants"} = render partial: "shop/shop/variant" + %input.button.right{type: :submit, value: "Add to Cart"} diff --git a/app/views/shop/shop/_variant.html.haml b/app/views/shop/shop/_variant.html.haml index 3dade13410..15d5485090 100644 --- a/app/views/shop/shop/_variant.html.haml +++ b/app/views/shop/shop/_variant.html.haml @@ -1,11 +1,12 @@ %td %td.notes -%td {{variant.options_text}} +%td{bindonce: ""} {{variant.options_text}} %td %input{type: :number, value: nil, min: 0, + "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}", "ng-model" => "variant.quantity"} @@ -13,8 +14,9 @@ %span{"ng-show" => "product.group_buy"} %input{type: :number, min: 0, + "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variant_attributes[{{variant.id}}][max_quantity]", "ng-model" => "variant.max_quantity"} -%td.price.text-right +%td.price.text-right{bindonce: ""} {{ variant.price | currency }} diff --git a/app/views/spree/api/orders/bulk_show.v1.rabl b/app/views/spree/api/orders/bulk_show.v1.rabl index 09c8cc3dfd..9addbbec6f 100644 --- a/app/views/spree/api/orders/bulk_show.v1.rabl +++ b/app/views/spree/api/orders/bulk_show.v1.rabl @@ -8,7 +8,7 @@ node( :completed_at ) { |order| order.completed_at.blank? ? "" : order.completed node( :distributor ) { |order| partial 'api/enterprises/bulk_show', :object => order.distributor } node( :order_cycle ) { |order| partial 'api/order_cycles/bulk_show', :object => order.order_cycle } node( :line_items ) do |order| - order.line_items.order('id ASC').map do |line_item| + order.line_items.managed_by(@current_api_user).order('id ASC').map do |line_item| partial 'spree/api/line_items/bulk_show', :object => line_item end end \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index 1fdfe13665..d74ed6256a 100644 --- a/config/database.yml +++ b/config/database.yml @@ -31,4 +31,4 @@ staging: database: open_food_network_prod pool: 5 username: ofn - password: f00d \ No newline at end of file + password: f00d diff --git a/config/initializers/angular_assets.rb b/config/initializers/angular_assets.rb new file mode 100644 index 0000000000..3109b43483 --- /dev/null +++ b/config/initializers/angular_assets.rb @@ -0,0 +1 @@ +Rails.application.assets.register_engine('.haml', Tilt::HamlTemplate) diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 98357b4cac..e4d7fef84d 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -5,11 +5,13 @@ module.exports = function(config) { frameworks: ['jasmine'], files: [ + APPLICATION_SPEC, 'app/assets/javascripts/shared/jquery-1.8.0.js', // TODO: Can we link to Rails' jquery? - 'app/assets/javascripts/shared/angular.js', - 'app/assets/javascripts/shared/angular-*.js', 'app/assets/javascripts/shared/jquery.timeago.js', 'app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js', + 'app/assets/javascripts/shared/angular-local-storage.js', + 'app/assets/javascripts/shared/bindonce.min.js', + 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', 'app/assets/javascripts/admin/shared_directives.js.coffee', 'app/assets/javascripts/admin/shared_services.js.coffee', @@ -19,7 +21,6 @@ module.exports = function(config) { 'app/assets/javascripts/admin/bulk_product_update.js.coffee', 'app/assets/javascripts/darkswarm/*.js*', 'app/assets/javascripts/darkswarm/**/*.js*', - 'spec/javascripts/unit/**/*.js*' ], diff --git a/config/routes.rb b/config/routes.rb index 301315857b..d514641701 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ Openfoodnetwork::Application.routes.draw do - root :to => 'home#temp_landing_page' + root :to => 'home#index' resource :shop, controller: "shop/shop" do get :products @@ -7,6 +7,8 @@ Openfoodnetwork::Application.routes.draw do get :order_cycle end + resources :producers, only: :index + namespace :shop do get '/checkout', :to => 'checkout#edit' , :as => :checkout put '/checkout', :to => 'checkout#update' , :as => :update_checkout @@ -51,14 +53,15 @@ Openfoodnetwork::Application.routes.draw do namespace :api do resources :enterprises do get :managed, on: :collection + get :accessible, on: :collection end resources :order_cycles do get :managed, on: :collection + get :accessible, on: :collection end end get "new_landing_page", :controller => 'home', :action => "new_landing_page" - get "darkswarm", controller: :darkswarm, action: :index get "about_us", :controller => 'home', :action => "about_us" namespace :open_food_network do @@ -69,6 +72,7 @@ Openfoodnetwork::Application.routes.draw do # Mount Spree's routes mount Spree::Core::Engine, :at => '/' + end @@ -96,6 +100,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', :to => 'admin/overview#index', :as => :admin namespace :api, :defaults => { :format => 'json' } do @@ -105,6 +110,10 @@ Spree::Core::Engine.routes.prepend do resources :products do get :managed, on: :collection + + resources :variants do + delete :soft_delete + end end resources :orders do @@ -126,4 +135,5 @@ Spree::Core::Engine.routes.prepend do get :clear, :on => :collection get :order_cycle_expired, :on => :collection end + end diff --git a/db/migrate/20140402033428_add_foreign_keys.rb b/db/migrate/20140402033428_add_foreign_keys.rb new file mode 100644 index 0000000000..f072127158 --- /dev/null +++ b/db/migrate/20140402033428_add_foreign_keys.rb @@ -0,0 +1,179 @@ +class AddForeignKeys < ActiveRecord::Migration + class AdjustmentMetadata < ActiveRecord::Base; end + class ExchangeVariant < ActiveRecord::Base; end + class Spree::InventoryUnit < ActiveRecord::Base; end + class Spree::LineItem < ActiveRecord::Base; end + class Spree::Address < ActiveRecord::Base; end + class Spree::Order < ActiveRecord::Base; end + class Spree::Taxon < ActiveRecord::Base; end + class CoordinatorFee < ActiveRecord::Base; end + + def change + setup_foreign_keys + end + + # http://stackoverflow.com/a/7679513/2720566 + def migrate(direction) + sanitise_data if direction == :up + super + end + + + private + + def sanitise_data + # Remove orphaned AdjustmentMetadata records + orphaned_adjustment_metadata = AdjustmentMetadata.joins('LEFT OUTER JOIN spree_adjustments ON spree_adjustments.id = adjustment_metadata.adjustment_id').where('spree_adjustments.id IS NULL') + say "Destroying #{orphaned_adjustment_metadata.count} orphaned AdjustmentMetadata (of total #{AdjustmentMetadata.count})" + orphaned_adjustment_metadata.destroy_all + + # Remove orphaned ExchangeVariants + orphaned_exchange_variants = ExchangeVariant.joins('LEFT OUTER JOIN spree_variants ON spree_variants.id=exchange_variants.variant_id').where('spree_variants.id IS NULL') + say "Destroying #{orphaned_exchange_variants.count} orphaned ExchangeVariants (of total #{ExchangeVariant.count})" + orphaned_exchange_variants.destroy_all + + # Remove orphaned Spree::InventoryUnits + orphaned_inventory_units = Spree::InventoryUnit.joins('LEFT OUTER JOIN spree_variants ON spree_variants.id=spree_inventory_units.variant_id').where('spree_variants.id IS NULL') + say "Destroying #{orphaned_inventory_units.count} orphaned InventoryUnits (of total #{Spree::InventoryUnit.count})" + orphaned_inventory_units.destroy_all + + # Remove orphaned Spree::LineItems + orphaned_line_items = Spree::LineItem. + joins('LEFT OUTER JOIN spree_variants ON spree_variants.id=spree_line_items.variant_id'). + joins('LEFT OUTER JOIN spree_orders ON spree_orders.id=spree_line_items.order_id'). + where('spree_variants.id IS NULL OR spree_orders.id IS NULL') + say "Destroying #{orphaned_line_items.count} orphaned LineItems (of total #{Spree::LineItem.count})" + orphaned_line_items.each { |li| li.delete } + + # Update orders without a distributor with a dummy distributor + state = Spree::State.first + country = state.andand.country + unless country && state + country = Spree::Country.create! name: 'Australia', iso_name: 'AU' + state = country.states.create! name: 'Victoria' + end + + address = Spree::Address.create!(firstname: 'Dummy distributor', lastname: 'Dummy distributor', phone: '12345678', state: state, + address1: 'Dummy distributor', city: 'Dummy distributor', zipcode: '1234', country: country) + deleted_distributor = Enterprise.create!(name: "Deleted distributor", address: address) + + orphaned_orders = Spree::Order.joins('LEFT OUTER JOIN enterprises ON enterprises.id=spree_orders.distributor_id').where('enterprises.id IS NULL') + say "Assigning a dummy distributor to #{orphaned_orders.count} orders with a deleted distributor (of total #{Spree::Order.count})" + orphaned_orders.update_all distributor_id: deleted_distributor.id + + # Remove orphaned Spree::Taxons + orphaned_taxons = Spree::Taxon.joins('LEFT OUTER JOIN spree_taxonomies ON spree_taxonomies.id=spree_taxons.taxonomy_id').where('spree_taxonomies.id IS NULL') + say "Destroying #{orphaned_taxons.count} orphaned Taxons (of total #{Spree::Taxon.count})" + orphaned_taxons.destroy_all + + # Remove orphaned CoordinatorFee records + orphaned_coordinator_fees = CoordinatorFee.joins('LEFT OUTER JOIN enterprise_fees ON enterprise_fees.id = coordinator_fees.enterprise_fee_id').where('enterprise_fees.id IS NULL') + say "Destroying #{orphaned_coordinator_fees.count} orphaned CoordinatorFees (of total #{CoordinatorFee.count})" + orphaned_coordinator_fees.each do |cf| + CoordinatorFee.connection.execute("DELETE FROM coordinator_fees WHERE coordinator_fees.order_cycle_id=#{cf.order_cycle_id} AND coordinator_fees.enterprise_fee_id=#{cf.enterprise_fee_id}") + end + end + + + def setup_foreign_keys + add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id" + add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" + add_foreign_key "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" + add_foreign_key "cms_blocks", "cms_pages", name: "cms_blocks_page_id_fk", column: "page_id" + add_foreign_key "cms_categories", "cms_sites", name: "cms_categories_site_id_fk", column: "site_id", dependent: :delete + add_foreign_key "cms_categorizations", "cms_categories", name: "cms_categorizations_category_id_fk", column: "category_id" + add_foreign_key "cms_files", "cms_blocks", name: "cms_files_block_id_fk", column: "block_id" + add_foreign_key "cms_files", "cms_sites", name: "cms_files_site_id_fk", column: "site_id" + add_foreign_key "cms_layouts", "cms_layouts", name: "cms_layouts_parent_id_fk", column: "parent_id" + add_foreign_key "cms_layouts", "cms_sites", name: "cms_layouts_site_id_fk", column: "site_id", dependent: :delete + add_foreign_key "cms_pages", "cms_layouts", name: "cms_pages_layout_id_fk", column: "layout_id" + add_foreign_key "cms_pages", "cms_pages", name: "cms_pages_parent_id_fk", column: "parent_id" + add_foreign_key "cms_pages", "cms_sites", name: "cms_pages_site_id_fk", column: "site_id", dependent: :delete + add_foreign_key "cms_pages", "cms_pages", name: "cms_pages_target_page_id_fk", column: "target_page_id" + add_foreign_key "cms_snippets", "cms_sites", name: "cms_snippets_site_id_fk", column: "site_id", dependent: :delete + add_foreign_key "coordinator_fees", "enterprise_fees", name: "coordinator_fees_enterprise_fee_id_fk" + add_foreign_key "coordinator_fees", "order_cycles", name: "coordinator_fees_order_cycle_id_fk" + add_foreign_key "distributors_payment_methods", "enterprises", name: "distributors_payment_methods_distributor_id_fk", column: "distributor_id" + add_foreign_key "distributors_payment_methods", "spree_payment_methods", name: "distributors_payment_methods_payment_method_id_fk", column: "payment_method_id" + add_foreign_key "distributors_shipping_methods", "enterprises", name: "distributors_shipping_methods_distributor_id_fk", column: "distributor_id" + add_foreign_key "distributors_shipping_methods", "spree_shipping_methods", name: "distributors_shipping_methods_shipping_method_id_fk", column: "shipping_method_id" + add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk" + add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" + add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" + add_foreign_key "enterprise_roles", "enterprises", name: "enterprise_roles_enterprise_id_fk" + add_foreign_key "enterprise_roles", "spree_users", name: "enterprise_roles_user_id_fk", column: "user_id" + add_foreign_key "enterprises", "spree_addresses", name: "enterprises_address_id_fk", column: "address_id" + add_foreign_key "exchange_fees", "enterprise_fees", name: "exchange_fees_enterprise_fee_id_fk" + add_foreign_key "exchange_fees", "exchanges", name: "exchange_fees_exchange_id_fk" + add_foreign_key "exchange_variants", "exchanges", name: "exchange_variants_exchange_id_fk" + add_foreign_key "exchange_variants", "spree_variants", name: "exchange_variants_variant_id_fk", column: "variant_id" + add_foreign_key "exchanges", "order_cycles", name: "exchanges_order_cycle_id_fk" + add_foreign_key "exchanges", "enterprises", name: "exchanges_payment_enterprise_id_fk", column: "payment_enterprise_id" + add_foreign_key "exchanges", "enterprises", name: "exchanges_receiver_id_fk", column: "receiver_id" + add_foreign_key "exchanges", "enterprises", name: "exchanges_sender_id_fk", column: "sender_id" + add_foreign_key "order_cycles", "enterprises", name: "order_cycles_coordinator_id_fk", column: "coordinator_id" + add_foreign_key "product_distributions", "enterprises", name: "product_distributions_distributor_id_fk", column: "distributor_id" + add_foreign_key "product_distributions", "enterprise_fees", name: "product_distributions_enterprise_fee_id_fk" + add_foreign_key "product_distributions", "spree_products", name: "product_distributions_product_id_fk", column: "product_id" + add_foreign_key "spree_addresses", "spree_countries", name: "spree_addresses_country_id_fk", column: "country_id" + add_foreign_key "spree_addresses", "spree_states", name: "spree_addresses_state_id_fk", column: "state_id" + add_foreign_key "spree_inventory_units", "spree_orders", name: "spree_inventory_units_order_id_fk", column: "order_id" + add_foreign_key "spree_inventory_units", "spree_return_authorizations", name: "spree_inventory_units_return_authorization_id_fk", column: "return_authorization_id" + add_foreign_key "spree_inventory_units", "spree_shipments", name: "spree_inventory_units_shipment_id_fk", column: "shipment_id" + add_foreign_key "spree_inventory_units", "spree_variants", name: "spree_inventory_units_variant_id_fk", column: "variant_id" + add_foreign_key "spree_line_items", "spree_orders", name: "spree_line_items_order_id_fk", column: "order_id" + add_foreign_key "spree_line_items", "spree_variants", name: "spree_line_items_variant_id_fk", column: "variant_id" + add_foreign_key "spree_option_types_prototypes", "spree_option_types", name: "spree_option_types_prototypes_option_type_id_fk", column: "option_type_id" + add_foreign_key "spree_option_types_prototypes", "spree_prototypes", name: "spree_option_types_prototypes_prototype_id_fk", column: "prototype_id" + add_foreign_key "spree_option_values", "spree_option_types", name: "spree_option_values_option_type_id_fk", column: "option_type_id" + add_foreign_key "spree_option_values_variants", "spree_option_values", name: "spree_option_values_variants_option_value_id_fk", column: "option_value_id" + 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", "spree_addresses", name: "spree_orders_bill_address_id_fk", column: "bill_address_id" + add_foreign_key "spree_orders", "carts", name: "spree_orders_cart_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_ship_address_id_fk", column: "ship_address_id" + #add_foreign_key "spree_orders", "spree_shipping_methods", name: "spree_orders_shipping_method_id_fk", column: "shipping_method_id" + add_foreign_key "spree_orders", "spree_users", name: "spree_orders_user_id_fk", column: "user_id" + add_foreign_key "spree_payments", "spree_orders", name: "spree_payments_order_id_fk", column: "order_id" + add_foreign_key "spree_payments", "spree_payment_methods", name: "spree_payments_payment_method_id_fk", column: "payment_method_id" + #add_foreign_key "spree_payments", "spree_payments", name: "spree_payments_source_id_fk", column: "source_id" + add_foreign_key "spree_prices", "spree_variants", name: "spree_prices_variant_id_fk", column: "variant_id" + add_foreign_key "spree_product_option_types", "spree_option_types", name: "spree_product_option_types_option_type_id_fk", column: "option_type_id" + add_foreign_key "spree_product_option_types", "spree_products", name: "spree_product_option_types_product_id_fk", column: "product_id" + add_foreign_key "spree_product_properties", "spree_products", name: "spree_product_properties_product_id_fk", column: "product_id" + add_foreign_key "spree_product_properties", "spree_properties", name: "spree_product_properties_property_id_fk", column: "property_id" + add_foreign_key "spree_products_promotion_rules", "spree_products", name: "spree_products_promotion_rules_product_id_fk", column: "product_id" + add_foreign_key "spree_products_promotion_rules", "spree_promotion_rules", name: "spree_products_promotion_rules_promotion_rule_id_fk", column: "promotion_rule_id" + add_foreign_key "spree_products", "spree_shipping_categories", name: "spree_products_shipping_category_id_fk", column: "shipping_category_id" + add_foreign_key "spree_products", "enterprises", name: "spree_products_supplier_id_fk", column: "supplier_id" + add_foreign_key "spree_products", "spree_tax_categories", name: "spree_products_tax_category_id_fk", column: "tax_category_id" + add_foreign_key "spree_products_taxons", "spree_products", name: "spree_products_taxons_product_id_fk", column: "product_id", dependent: :delete + add_foreign_key "spree_products_taxons", "spree_taxons", name: "spree_products_taxons_taxon_id_fk", column: "taxon_id", dependent: :delete + add_foreign_key "spree_promotion_action_line_items", "spree_promotion_actions", name: "spree_promotion_action_line_items_promotion_action_id_fk", column: "promotion_action_id" + add_foreign_key "spree_promotion_action_line_items", "spree_variants", name: "spree_promotion_action_line_items_variant_id_fk", column: "variant_id" + add_foreign_key "spree_promotion_actions", "spree_activators", name: "spree_promotion_actions_activator_id_fk", column: "activator_id" + add_foreign_key "spree_promotion_rules", "spree_activators", name: "spree_promotion_rules_activator_id_fk", column: "activator_id" + add_foreign_key "spree_properties_prototypes", "spree_properties", name: "spree_properties_prototypes_property_id_fk", column: "property_id" + add_foreign_key "spree_properties_prototypes", "spree_prototypes", name: "spree_properties_prototypes_prototype_id_fk", column: "prototype_id" + add_foreign_key "spree_return_authorizations", "spree_orders", name: "spree_return_authorizations_order_id_fk", column: "order_id" + add_foreign_key "spree_roles_users", "spree_roles", name: "spree_roles_users_role_id_fk", column: "role_id" + add_foreign_key "spree_roles_users", "spree_users", name: "spree_roles_users_user_id_fk", column: "user_id" + add_foreign_key "spree_shipments", "spree_addresses", name: "spree_shipments_address_id_fk", column: "address_id" + add_foreign_key "spree_shipments", "spree_orders", name: "spree_shipments_order_id_fk", column: "order_id" + #add_foreign_key "spree_shipments", "spree_shipping_methods", name: "spree_shipments_shipping_method_id_fk", column: "shipping_method_id" + add_foreign_key "spree_shipping_methods", "spree_shipping_categories", name: "spree_shipping_methods_shipping_category_id_fk", column: "shipping_category_id" + add_foreign_key "spree_shipping_methods", "spree_zones", name: "spree_shipping_methods_zone_id_fk", column: "zone_id" + add_foreign_key "spree_state_changes", "spree_users", name: "spree_state_changes_user_id_fk", column: "user_id" + add_foreign_key "spree_states", "spree_countries", name: "spree_states_country_id_fk", column: "country_id" + add_foreign_key "spree_tax_rates", "spree_tax_categories", name: "spree_tax_rates_tax_category_id_fk", column: "tax_category_id" + add_foreign_key "spree_tax_rates", "spree_zones", name: "spree_tax_rates_zone_id_fk", column: "zone_id" + add_foreign_key "spree_taxons", "spree_taxons", name: "spree_taxons_parent_id_fk", column: "parent_id" + add_foreign_key "spree_taxons", "spree_taxonomies", name: "spree_taxons_taxonomy_id_fk", column: "taxonomy_id" + add_foreign_key "spree_users", "spree_addresses", name: "spree_users_bill_address_id_fk", column: "bill_address_id" + add_foreign_key "spree_users", "spree_addresses", name: "spree_users_ship_address_id_fk", column: "ship_address_id" + add_foreign_key "spree_variants", "spree_products", name: "spree_variants_product_id_fk", column: "product_id" + add_foreign_key "spree_zone_members", "spree_zones", name: "spree_zone_members_zone_id_fk", column: "zone_id" + add_foreign_key "suburbs", "spree_states", name: "suburbs_state_id_fk", column: "state_id" + end +end diff --git a/db/migrate/20140425055718_add_active_flag_to_enterprises.rb b/db/migrate/20140425055718_add_active_flag_to_enterprises.rb new file mode 100644 index 0000000000..b0bcc2f3ed --- /dev/null +++ b/db/migrate/20140425055718_add_active_flag_to_enterprises.rb @@ -0,0 +1,5 @@ +class AddActiveFlagToEnterprises < ActiveRecord::Migration + def change + add_column :enterprises, :active, :boolean, default: true + end +end diff --git a/db/migrate/20140430020639_rename_active_to_visible_on_enterprises.rb b/db/migrate/20140430020639_rename_active_to_visible_on_enterprises.rb new file mode 100644 index 0000000000..99b011304a --- /dev/null +++ b/db/migrate/20140430020639_rename_active_to_visible_on_enterprises.rb @@ -0,0 +1,5 @@ +class RenameActiveToVisibleOnEnterprises < ActiveRecord::Migration + def change + rename_column :enterprises, :active, :visible + end +end diff --git a/db/schema.rb b/db/schema.rb index 83688ff525..5df4ad8e56 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 => 20140402032034) do +ActiveRecord::Schema.define(:version => 20140430020639) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -219,8 +219,8 @@ ActiveRecord::Schema.define(:version => 20140402032034) do t.integer "address_id" t.string "pickup_times" t.string "next_collection_at" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "distributor_info" t.string "logo_file_name" t.string "logo_content_type" @@ -230,6 +230,7 @@ ActiveRecord::Schema.define(:version => 20140402032034) do t.string "promo_image_content_type" t.integer "promo_image_file_size" t.datetime "promo_image_updated_at" + t.boolean "visible", :default => true end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -966,4 +967,151 @@ ActiveRecord::Schema.define(:version => 20140402032034) do t.integer "state_id" end + 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 "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" + + add_foreign_key "cms_blocks", "cms_pages", name: "cms_blocks_page_id_fk", column: "page_id" + + add_foreign_key "cms_categories", "cms_sites", name: "cms_categories_site_id_fk", column: "site_id", dependent: :delete + + add_foreign_key "cms_categorizations", "cms_categories", name: "cms_categorizations_category_id_fk", column: "category_id" + + add_foreign_key "cms_files", "cms_blocks", name: "cms_files_block_id_fk", column: "block_id" + add_foreign_key "cms_files", "cms_sites", name: "cms_files_site_id_fk", column: "site_id" + + add_foreign_key "cms_layouts", "cms_layouts", name: "cms_layouts_parent_id_fk", column: "parent_id" + add_foreign_key "cms_layouts", "cms_sites", name: "cms_layouts_site_id_fk", column: "site_id", dependent: :delete + + add_foreign_key "cms_pages", "cms_layouts", name: "cms_pages_layout_id_fk", column: "layout_id" + add_foreign_key "cms_pages", "cms_pages", name: "cms_pages_parent_id_fk", column: "parent_id" + add_foreign_key "cms_pages", "cms_pages", name: "cms_pages_target_page_id_fk", column: "target_page_id" + add_foreign_key "cms_pages", "cms_sites", name: "cms_pages_site_id_fk", column: "site_id", dependent: :delete + + add_foreign_key "cms_snippets", "cms_sites", name: "cms_snippets_site_id_fk", column: "site_id", dependent: :delete + + add_foreign_key "coordinator_fees", "enterprise_fees", name: "coordinator_fees_enterprise_fee_id_fk" + add_foreign_key "coordinator_fees", "order_cycles", name: "coordinator_fees_order_cycle_id_fk" + + add_foreign_key "distributors_payment_methods", "enterprises", name: "distributors_payment_methods_distributor_id_fk", column: "distributor_id" + add_foreign_key "distributors_payment_methods", "spree_payment_methods", name: "distributors_payment_methods_payment_method_id_fk", column: "payment_method_id" + + add_foreign_key "distributors_shipping_methods", "enterprises", name: "distributors_shipping_methods_distributor_id_fk", column: "distributor_id" + add_foreign_key "distributors_shipping_methods", "spree_shipping_methods", name: "distributors_shipping_methods_shipping_method_id_fk", column: "shipping_method_id" + + add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk" + + add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" + add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" + + add_foreign_key "enterprise_roles", "enterprises", name: "enterprise_roles_enterprise_id_fk" + add_foreign_key "enterprise_roles", "spree_users", name: "enterprise_roles_user_id_fk", column: "user_id" + + add_foreign_key "enterprises", "spree_addresses", name: "enterprises_address_id_fk", column: "address_id" + + add_foreign_key "exchange_fees", "enterprise_fees", name: "exchange_fees_enterprise_fee_id_fk" + add_foreign_key "exchange_fees", "exchanges", name: "exchange_fees_exchange_id_fk" + + add_foreign_key "exchange_variants", "exchanges", name: "exchange_variants_exchange_id_fk" + add_foreign_key "exchange_variants", "spree_variants", name: "exchange_variants_variant_id_fk", column: "variant_id" + + add_foreign_key "exchanges", "enterprises", name: "exchanges_payment_enterprise_id_fk", column: "payment_enterprise_id" + add_foreign_key "exchanges", "enterprises", name: "exchanges_receiver_id_fk", column: "receiver_id" + add_foreign_key "exchanges", "enterprises", name: "exchanges_sender_id_fk", column: "sender_id" + add_foreign_key "exchanges", "order_cycles", name: "exchanges_order_cycle_id_fk" + + add_foreign_key "order_cycles", "enterprises", name: "order_cycles_coordinator_id_fk", column: "coordinator_id" + + add_foreign_key "product_distributions", "enterprise_fees", name: "product_distributions_enterprise_fee_id_fk" + add_foreign_key "product_distributions", "enterprises", name: "product_distributions_distributor_id_fk", column: "distributor_id" + add_foreign_key "product_distributions", "spree_products", name: "product_distributions_product_id_fk", column: "product_id" + + add_foreign_key "spree_addresses", "spree_countries", name: "spree_addresses_country_id_fk", column: "country_id" + add_foreign_key "spree_addresses", "spree_states", name: "spree_addresses_state_id_fk", column: "state_id" + + add_foreign_key "spree_inventory_units", "spree_orders", name: "spree_inventory_units_order_id_fk", column: "order_id" + add_foreign_key "spree_inventory_units", "spree_return_authorizations", name: "spree_inventory_units_return_authorization_id_fk", column: "return_authorization_id" + add_foreign_key "spree_inventory_units", "spree_shipments", name: "spree_inventory_units_shipment_id_fk", column: "shipment_id" + add_foreign_key "spree_inventory_units", "spree_variants", name: "spree_inventory_units_variant_id_fk", column: "variant_id" + + add_foreign_key "spree_line_items", "spree_orders", name: "spree_line_items_order_id_fk", column: "order_id" + add_foreign_key "spree_line_items", "spree_variants", name: "spree_line_items_variant_id_fk", column: "variant_id" + + add_foreign_key "spree_option_types_prototypes", "spree_option_types", name: "spree_option_types_prototypes_option_type_id_fk", column: "option_type_id" + add_foreign_key "spree_option_types_prototypes", "spree_prototypes", name: "spree_option_types_prototypes_prototype_id_fk", column: "prototype_id" + + add_foreign_key "spree_option_values", "spree_option_types", name: "spree_option_values_option_type_id_fk", column: "option_type_id" + + add_foreign_key "spree_option_values_variants", "spree_option_values", name: "spree_option_values_variants_option_value_id_fk", column: "option_value_id" + 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", "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" + add_foreign_key "spree_orders", "spree_addresses", name: "spree_orders_ship_address_id_fk", column: "ship_address_id" + add_foreign_key "spree_orders", "spree_users", name: "spree_orders_user_id_fk", column: "user_id" + + add_foreign_key "spree_payments", "spree_orders", name: "spree_payments_order_id_fk", column: "order_id" + add_foreign_key "spree_payments", "spree_payment_methods", name: "spree_payments_payment_method_id_fk", column: "payment_method_id" + + add_foreign_key "spree_prices", "spree_variants", name: "spree_prices_variant_id_fk", column: "variant_id" + + add_foreign_key "spree_product_option_types", "spree_option_types", name: "spree_product_option_types_option_type_id_fk", column: "option_type_id" + add_foreign_key "spree_product_option_types", "spree_products", name: "spree_product_option_types_product_id_fk", column: "product_id" + + add_foreign_key "spree_product_properties", "spree_products", name: "spree_product_properties_product_id_fk", column: "product_id" + add_foreign_key "spree_product_properties", "spree_properties", name: "spree_product_properties_property_id_fk", column: "property_id" + + add_foreign_key "spree_products", "enterprises", name: "spree_products_supplier_id_fk", column: "supplier_id" + add_foreign_key "spree_products", "spree_shipping_categories", name: "spree_products_shipping_category_id_fk", column: "shipping_category_id" + add_foreign_key "spree_products", "spree_tax_categories", name: "spree_products_tax_category_id_fk", column: "tax_category_id" + + add_foreign_key "spree_products_promotion_rules", "spree_products", name: "spree_products_promotion_rules_product_id_fk", column: "product_id" + add_foreign_key "spree_products_promotion_rules", "spree_promotion_rules", name: "spree_products_promotion_rules_promotion_rule_id_fk", column: "promotion_rule_id" + + add_foreign_key "spree_products_taxons", "spree_products", name: "spree_products_taxons_product_id_fk", column: "product_id", dependent: :delete + add_foreign_key "spree_products_taxons", "spree_taxons", name: "spree_products_taxons_taxon_id_fk", column: "taxon_id", dependent: :delete + + add_foreign_key "spree_promotion_action_line_items", "spree_promotion_actions", name: "spree_promotion_action_line_items_promotion_action_id_fk", column: "promotion_action_id" + add_foreign_key "spree_promotion_action_line_items", "spree_variants", name: "spree_promotion_action_line_items_variant_id_fk", column: "variant_id" + + add_foreign_key "spree_promotion_actions", "spree_activators", name: "spree_promotion_actions_activator_id_fk", column: "activator_id" + + add_foreign_key "spree_promotion_rules", "spree_activators", name: "spree_promotion_rules_activator_id_fk", column: "activator_id" + + add_foreign_key "spree_properties_prototypes", "spree_properties", name: "spree_properties_prototypes_property_id_fk", column: "property_id" + add_foreign_key "spree_properties_prototypes", "spree_prototypes", name: "spree_properties_prototypes_prototype_id_fk", column: "prototype_id" + + add_foreign_key "spree_return_authorizations", "spree_orders", name: "spree_return_authorizations_order_id_fk", column: "order_id" + + add_foreign_key "spree_roles_users", "spree_roles", name: "spree_roles_users_role_id_fk", column: "role_id" + add_foreign_key "spree_roles_users", "spree_users", name: "spree_roles_users_user_id_fk", column: "user_id" + + add_foreign_key "spree_shipments", "spree_addresses", name: "spree_shipments_address_id_fk", column: "address_id" + add_foreign_key "spree_shipments", "spree_orders", name: "spree_shipments_order_id_fk", column: "order_id" + + add_foreign_key "spree_shipping_methods", "spree_shipping_categories", name: "spree_shipping_methods_shipping_category_id_fk", column: "shipping_category_id" + add_foreign_key "spree_shipping_methods", "spree_zones", name: "spree_shipping_methods_zone_id_fk", column: "zone_id" + + add_foreign_key "spree_state_changes", "spree_users", name: "spree_state_changes_user_id_fk", column: "user_id" + + add_foreign_key "spree_states", "spree_countries", name: "spree_states_country_id_fk", column: "country_id" + + add_foreign_key "spree_tax_rates", "spree_tax_categories", name: "spree_tax_rates_tax_category_id_fk", column: "tax_category_id" + add_foreign_key "spree_tax_rates", "spree_zones", name: "spree_tax_rates_zone_id_fk", column: "zone_id" + + add_foreign_key "spree_taxons", "spree_taxonomies", name: "spree_taxons_taxonomy_id_fk", column: "taxonomy_id" + add_foreign_key "spree_taxons", "spree_taxons", name: "spree_taxons_parent_id_fk", column: "parent_id" + + add_foreign_key "spree_users", "spree_addresses", name: "spree_users_bill_address_id_fk", column: "bill_address_id" + add_foreign_key "spree_users", "spree_addresses", name: "spree_users_ship_address_id_fk", column: "ship_address_id" + + add_foreign_key "spree_variants", "spree_products", name: "spree_variants_product_id_fk", column: "product_id" + + add_foreign_key "spree_zone_members", "spree_zones", name: "spree_zone_members_zone_id_fk", column: "zone_id" + + add_foreign_key "suburbs", "spree_states", name: "suburbs_state_id_fk", column: "state_id" + end diff --git a/lib/spree/api/testing_support/helpers_decorator.rb b/lib/spree/api/testing_support/helpers_decorator.rb new file mode 100644 index 0000000000..0ade798960 --- /dev/null +++ b/lib/spree/api/testing_support/helpers_decorator.rb @@ -0,0 +1,7 @@ +require 'spree/api/testing_support/helpers' + +Spree::Api::TestingSupport::Helpers.class_eval do + def current_api_user + @current_api_user ||= stub_model(Spree::LegacyUser, :email => "spree@example.com", :enterprises => []) + end +end diff --git a/lib/spree/api/testing_support/setup.rb b/lib/spree/api/testing_support/setup.rb new file mode 100644 index 0000000000..cbf8393e33 --- /dev/null +++ b/lib/spree/api/testing_support/setup.rb @@ -0,0 +1,43 @@ +module Spree + module Api + module TestingSupport + module Setup + def sign_in_as_user! + let!(:current_api_user) do + user = stub_model(Spree::LegacyUser) + user.stub(:has_spree_role?).with("admin").and_return(false) + user.stub(:enterprises) { [] } + user + end + end + + # enterprises is an array of variable names of let defines + # eg. + # let(:enterprise) { ... } + # sign_in_as_enterprise_user! [:enterprise] + def sign_in_as_enterprise_user!(enterprises) + let!(:current_api_user) do + user = create(:user) + user.spree_roles = [] + enterprises.each { |e| user.enterprise_roles.create(enterprise: send(e)) } + user.save! + user + end + end + + + def sign_in_as_admin! + let!(:current_api_user) do + user = stub_model(Spree::LegacyUser) + user.stub(:has_spree_role?).with("admin").and_return(true) + + # Stub enterprises, needed for cancan ability checks + user.stub(:enterprises) { [] } + + user + end + end + end + end + end +end diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake new file mode 100644 index 0000000000..33a1d05dd4 --- /dev/null +++ b/lib/tasks/karma.rake @@ -0,0 +1,33 @@ +namespace :karma do + task :start => :environment do + with_tmp_config :start + end + + task :run => :environment do + with_tmp_config :start, "--single-run" + end + + private + + def with_tmp_config(command, args = nil) + Tempfile.open('karma_unit.js', Rails.root.join('tmp') ) do |f| + f.write unit_js(application_spec_files) + f.flush + trap('SIGINT') { puts "Killing Karma"; exit } + exec "karma #{command} #{f.path} #{args}" + #%x{karma #{command} #{f.path} #{args}} + end + end + + def application_spec_files + sprockets = Rails.application.assets + sprockets.append_path Rails.root.join("spec/javascripts") + files = Rails.application.assets.find_asset("application_spec.js").to_a.map {|e| e.pathname.to_s } + end + + def unit_js(files) + puts files + unit_js = File.open('config/ng-test.conf.js', 'r').read + unit_js.gsub "APPLICATION_SPEC", "\"#{files.join("\",\n\"")}\"" + end +end diff --git a/spec/controllers/api/order_cycles_controller_spec.rb b/spec/controllers/api/order_cycles_controller_spec.rb index 3c562840f5..7f8bbec7af 100644 --- a/spec/controllers/api/order_cycles_controller_spec.rb +++ b/spec/controllers/api/order_cycles_controller_spec.rb @@ -8,6 +8,7 @@ module Api let!(:oc1) { FactoryGirl.create(:order_cycle) } let!(:oc2) { FactoryGirl.create(:order_cycle) } + let(:coordinator) { oc1.coordinator } let(:attributes) { [:id, :name, :suppliers, :distributors] } before do @@ -16,11 +17,98 @@ module Api end context "as a normal user" do + sign_in_as_user! + + it "should deny me access to managed order cycles" do + spree_get :managed, { :format => :json } + assert_unauthorized! + end + end + + context "as an enterprise user" do + sign_in_as_enterprise_user! [:coordinator] + it "retrieves a list of variants with appropriate attributes" do get :managed, { :format => :json } keys = json_response.first.keys.map{ |key| key.to_sym } attributes.all?{ |attr| keys.include? attr }.should == true end end + + context "as an administrator" do + sign_in_as_admin! + + it "retrieves a list of variants with appropriate attributes" do + get :managed, { :format => :json } + keys = json_response.first.keys.map{ |key| key.to_sym } + attributes.all?{ |attr| keys.include? attr }.should == true + end + end + + context "using the accessible action to list order cycles" do + let(:oc_supplier) { create(:supplier_enterprise) } + let(:oc_distributor) { create(:distributor_enterprise) } + let(:other_supplier) { create(:supplier_enterprise) } + let(:oc_supplier_user) do + user = create(:user) + user.spree_roles = [] + user.enterprise_roles.create(enterprise: oc_supplier) + user.save! + user + end + let(:oc_distributor_user) do + user = create(:user) + user.spree_roles = [] + user.enterprise_roles.create(enterprise: oc_distributor) + user.save! + user + end + let(:other_supplier_user) do + user = create(:user) + user.spree_roles = [] + user.enterprise_roles.create(enterprise: other_supplier) + user.save! + user + end + let!(:order_cycle) { create(:order_cycle, suppliers: [oc_supplier], distributors: [oc_distributor]) } + + context "as the user of a supplier to an order cycle" do + before :each do + stub_authentication! + Spree.user_class.stub :find_by_spree_api_key => oc_supplier_user + spree_get :accessible, { :template => 'bulk_index', :format => :json } + end + + it "gives me access" do + json_response.length.should == 1 + json_response[0]['id'].should == order_cycle.id + end + end + + context "as the user of some other supplier" do + before :each do + stub_authentication! + Spree.user_class.stub :find_by_spree_api_key => other_supplier_user + spree_get :accessible, { :template => 'bulk_index', :format => :json } + end + + it "does not give me access" do + json_response.length.should == 0 + end + end + + context "as the user of a hub for the order cycle" do + before :each do + stub_authentication! + Spree.user_class.stub :find_by_spree_api_key => oc_distributor_user + spree_get :accessible, { :template => 'bulk_index', :format => :json } + end + + it "gives me access" do + json_response.length.should == 1 + json_response[0]['id'].should == order_cycle.id + end + end + end end -end \ No newline at end of file +end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 18be4a0700..2f4ad05d8f 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -1,80 +1,23 @@ require 'spec_helper' -describe Spree::HomeController do - it "loads products" do - product = create(:product) - spree_get :index - assigns(:products).should == [product] - assigns(:products_local).should be_nil - assigns(:products_remote).should be_nil +describe HomeController do + render_views + let(:distributor) { create(:distributor_enterprise) } + + before do + controller.stub(:load_data_for_sidebar).and_return nil + Enterprise.stub(:distributors_with_active_order_cycles).and_return [distributor] + Enterprise.stub(:is_distributor).and_return [distributor] end - - it "splits products by local/remote distributor when distributor is selected" do - # Given two distributors with a product under each - d1 = create(:distributor_enterprise) - d2 = create(:distributor_enterprise) - p1 = create(:product, :distributors => [d1]) - p2 = create(:product, :distributors => [d2]) - - # And the first distributor is selected - controller.stub(:current_distributor).and_return(d1) - - # When I fetch the home page, the products should be split by local/remote distributor - spree_get :index - assigns(:products).should be_nil - assigns(:products_local).should == [p1] - assigns(:products_remote).should == [p2] + it "sets active distributors" do + get :index + assigns[:active_distributors].should == [distributor] end - - context "BaseController: merging incomplete orders" do - it "loads the incomplete order when there is no current order" do - incomplete_order = double(:order, id: 1, distributor: 2, order_cycle: 3) - current_order = nil - - user = double(:user, last_incomplete_spree_order: incomplete_order) - controller.stub(:try_spree_current_user).and_return(user) - controller.stub(:current_order).and_return(current_order) - - incomplete_order.should_receive(:destroy).never - incomplete_order.should_receive(:merge!).never - - session[:order_id] = nil - spree_get :index - session[:order_id].should == incomplete_order.id - end - - it "destroys the incomplete order when there is a current order" do - oc = double(:order_cycle, closed?: false) - incomplete_order = double(:order, distributor: 1, order_cycle: oc) - current_order = double(:order, distributor: 1, order_cycle: oc) - - user = double(:user, last_incomplete_spree_order: incomplete_order) - controller.stub(:try_spree_current_user).and_return(user) - controller.stub(:current_order).and_return(current_order) - - incomplete_order.should_receive(:destroy) - incomplete_order.should_receive(:merge!).never - current_order.should_receive(:merge!).never - - session[:order_id] = 123 - - spree_get :index - end - end - - context "StoreController: handling order cycles expiring mid-order" do - it "clears the order and displays an expiry message" do - oc = double(:order_cycle, id: 123, closed?: true) - controller.stub(:current_order_cycle) { oc } - - order = double(:order) - order.should_receive(:empty!) - order.should_receive(:set_order_cycle!).with(nil) - controller.stub(:current_order) { order } - - spree_get :index - session[:expired_order_cycle_id].should == 123 - response.should redirect_to spree.order_cycle_expired_orders_path - end + + # This is done inside the json/hubs RABL template + it "gets the next order cycle for each hub" do + OrderCycle.should_receive(:first_closing_for).with(distributor) + get :index end end + diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index ed91818fbd..bdefa65ec2 100644 --- a/spec/controllers/shop/checkout_controller_spec.rb +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -8,6 +8,7 @@ describe Shop::CheckoutController do order.stub(:checkout_allowed?).and_return true controller.stub(:check_authorization).and_return true end + it "redirects home when no distributor is selected" do get :edit response.should redirect_to root_path @@ -51,6 +52,7 @@ describe Shop::CheckoutController do controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return(order) end + it "does not clone the ship address from distributor when shipping method requires address" do get :edit assigns[:order].ship_address.address1.should be_nil @@ -76,6 +78,36 @@ describe Shop::CheckoutController do end end + context "via xhr" do + before do + controller.stub(:current_distributor).and_return(distributor) + controller.stub(:current_order_cycle).and_return(order_cycle) + controller.stub(:current_order).and_return(order) + end + + it "returns errors" do + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 400 + response.body.should == {errors: assigns[:order].errors, flash: {}}.to_json + end + + it "returns flash" do + order.stub(:update_attributes).and_return true + order.stub(:next).and_return false + xhr :post, :update, order: {}, use_route: :spree + response.body.should == {errors: assigns[:order].errors, flash: {error: "Payment could not be processed, please check the details you entered"}}.to_json + end + + it "returns order confirmation url on success" do + order.stub(:update_attributes).and_return true + order.stub(:state).and_return "complete" + + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 200 + response.body.should == {path: spree.order_path(order)}.to_json + end + end + describe "Paypal routing" do let(:payment_method) { create(:payment_method, type: "Spree::BillingIntegration::PaypalExpress") } before do diff --git a/spec/controllers/spree/api/orders_controller_spec.rb b/spec/controllers/spree/api/orders_controller_spec.rb index 99b218a009..ec87169f76 100644 --- a/spec/controllers/spree/api/orders_controller_spec.rb +++ b/spec/controllers/spree/api/orders_controller_spec.rb @@ -5,24 +5,41 @@ module Spree describe Spree::Api::OrdersController do include Spree::Api::TestingSupport::Helpers render_views - - let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } - let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } - let(:order_attributes) { [:id, :full_name, :email, :phone, :completed_at, :line_items, :distributor, :order_cycle, :number] } - let(:line_item_attributes) { [:id, :quantity, :max_quantity, :supplier, :units_product, :units_variant] } before do stub_authentication! Spree.user_class.stub :find_by_spree_api_key => current_api_user end + let(:order_attributes) { [:id, :full_name, :email, :phone, :completed_at, :line_items, :distributor, :order_cycle, :number] } + + def self.make_simple_data! + let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } + let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } + let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } + let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } + let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } + let(:line_item_attributes) { [:id, :quantity, :max_quantity, :supplier, :units_product, :units_variant] } + end + + context "as a normal user" do + sign_in_as_user! + make_simple_data! + + it "should deny me access to managed orders" do + spree_get :managed, { :template => 'bulk_index', :format => :json } + assert_unauthorized! + end + end + + context "as an administrator" do + sign_in_as_admin! + make_simple_data! + before :each do spree_get :managed, { :template => 'bulk_index', :format => :json } end @@ -68,5 +85,45 @@ module Spree json_response.map{ |order| order['number'] }.all?{ |number| number.match("^R\\d{5,10}$") }.should == true end end + + context "as an enterprise user" do + let(:supplier) { create(:supplier_enterprise) } + let(:distributor1) { create(:distributor_enterprise) } + let(:distributor2) { create(:distributor_enterprise) } + let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:line_item2) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now, distributor: distributor2, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item3) { FactoryGirl.create(:line_item, order: order2, product: FactoryGirl.create(:product, supplier: supplier)) } + + context "producer enterprise" do + sign_in_as_enterprise_user! [:supplier] + + before :each do + spree_get :managed, { :template => 'bulk_index', :format => :json } + end + + it "does not display line item for which my enteprise is a supplier" do + response.status.should == 401 + end + end + + context "hub enterprise" do + sign_in_as_enterprise_user! [:distributor1] + + before :each do + spree_get :managed, { :template => 'bulk_index', :format => :json } + end + + it "retrieves a list of orders" do + keys = json_response.first.keys.map{ |key| key.to_sym } + order_attributes.all?{ |attr| keys.include? attr }.should == true + end + + it "only displays line items from orders for which my enterprise is a distributor" do + json_response.map{ |order| order['line_items'] }.flatten.map{ |line_item| line_item["id"] }.should == [line_item1.id, line_item2.id] + end + end + end end -end \ No newline at end of file +end diff --git a/spec/controllers/spree/api/products_controller_spec.rb b/spec/controllers/spree/api/products_controller_spec.rb index 0d7560aa1b..c64b95853f 100644 --- a/spec/controllers/spree/api/products_controller_spec.rb +++ b/spec/controllers/spree/api/products_controller_spec.rb @@ -6,9 +6,10 @@ module Spree include Spree::Api::TestingSupport::Helpers render_views - let!(:product1) { FactoryGirl.create(:product) } - let!(:product2) { FactoryGirl.create(:product) } - let!(:product3) { FactoryGirl.create(:product) } + let(:supplier) { FactoryGirl.create(:supplier_enterprise) } + let!(:product1) { FactoryGirl.create(:product, supplier: supplier) } + let!(:product2) { FactoryGirl.create(:product, supplier: supplier) } + let!(:product3) { FactoryGirl.create(:product, supplier: supplier) } let(:attributes) { [:id, :name, :supplier, :price, :on_hand, :available_on, :permalink_live] } let(:unit_attributes) { [:id, :name, :group_buy_unit_size, :variant_unit] } @@ -18,6 +19,37 @@ module Spree end context "as a normal user" do + sign_in_as_user! + + it "should deny me access to managed products" do + spree_get :managed, { :template => 'bulk_index', :format => :json } + assert_unauthorized! + end + end + + context "as an enterprise user" do + sign_in_as_enterprise_user! [:supplier] + + before :each do + spree_get :index, { :template => 'bulk_index', :format => :json } + end + + it "retrieves a list of managed products" do + spree_get :managed, { :template => 'bulk_index', :format => :json } + keys = json_response.first.keys.map{ |key| key.to_sym } + attributes.all?{ |attr| keys.include? attr }.should == true + end + end + + context "as an administrator" do + sign_in_as_admin! + + it "retrieves a list of managed products" do + spree_get :managed, { :template => 'bulk_index', :format => :json } + keys = json_response.first.keys.map{ |key| key.to_sym } + attributes.all?{ |attr| keys.include? attr }.should == true + end + it "retrieves a list of products with appropriate attributes" do spree_get :index, { :template => 'bulk_index', :format => :json } keys = json_response.first.keys.map{ |key| key.to_sym } @@ -61,4 +93,4 @@ module Spree end end end -end \ No newline at end of file +end diff --git a/spec/controllers/spree/api/variants_controller_spec.rb b/spec/controllers/spree/api/variants_controller_spec.rb index c61928e1bb..818e10457a 100644 --- a/spec/controllers/spree/api/variants_controller_spec.rb +++ b/spec/controllers/spree/api/variants_controller_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -require 'spree/api/testing_support/helpers' module Spree describe Spree::Api::VariantsController do - include Spree::Api::TestingSupport::Helpers render_views - + + let(:supplier) { FactoryGirl.create(:supplier_enterprise) } let!(:variant1) { FactoryGirl.create(:variant) } let!(:variant2) { FactoryGirl.create(:variant) } let!(:variant3) { FactoryGirl.create(:variant) } @@ -18,6 +17,8 @@ module Spree end context "as a normal user" do + sign_in_as_user! + it "retrieves a list of variants with appropriate attributes" do spree_get :index, { :template => 'bulk_index', :format => :json } keys = json_response.first.keys.map{ |key| key.to_sym } @@ -29,12 +30,53 @@ module Spree keys = json_response.keys.map{ |key| key.to_sym } unit_attributes.all?{ |attr| keys.include? attr }.should == true end - #it "sorts variants in ascending id order" do - # spree_get :index, { :template => 'bulk_index', :format => :json } - # ids = json_response.map{ |variant| variant['id'] } - # ids[0].should < ids[1] - # ids[1].should < ids[2] - #end + + it "is denied access when trying to delete a variant" do + product = create(:product) + variant = product.master + + spree_delete :soft_delete, {variant_id: variant.to_param, product_id: product.to_param, format: :json} + assert_unauthorized! + lambda { variant.reload }.should_not raise_error + variant.deleted_at.should be_nil + end + end + + context "as an enterprise user" do + sign_in_as_enterprise_user! [:supplier] + let(:supplier_other) { create(:supplier_enterprise) } + let(:product) { create(:product, supplier: supplier) } + let(:variant) { product.master } + let(:product_other) { create(:product, supplier: supplier_other) } + let(:variant_other) { product_other.master } + + it "soft deletes a variant" do + spree_delete :soft_delete, {variant_id: variant.to_param, product_id: product.to_param, format: :json} + response.status.should == 204 + lambda { variant.reload }.should_not raise_error + variant.deleted_at.should_not be_nil + end + + it "is denied access to soft deleting another enterprises' variant" do + spree_delete :soft_delete, {variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json} + assert_unauthorized! + lambda { variant.reload }.should_not raise_error + variant.deleted_at.should be_nil + end + end + + context "as an administrator" do + sign_in_as_admin! + + it "soft deletes a variant" do + product = create(:product) + variant = product.master + + spree_delete :soft_delete, {variant_id: variant.to_param, product_id: product.to_param, format: :json} + response.status.should == 204 + lambda { variant.reload }.should_not raise_error + variant.deleted_at.should_not be_nil + end end end -end \ No newline at end of file +end diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 0c7300dadc..e83b69eebd 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -9,7 +9,7 @@ feature %q{ before :all do @default_wait_time = Capybara.default_wait_time - Capybara.default_wait_time = 5 + Capybara.default_wait_time = 10 end after :all do @@ -526,7 +526,7 @@ feature %q{ before :each do visit '/admin/orders/bulk_management' within "tr#li_#{li3.id}" do - click_link li3.variant.options_text + find("a", text: li3.product.name + ": " + li3.variant.options_text).click end end @@ -561,7 +561,7 @@ feature %q{ context "clicking 'Clear' in group buy box" do before :each do - click_link 'Clear' + find("a", text: "Clear").click end it "shows all products and clears group buy box" do diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 3de9b6f1e1..0c8d81bd5c 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -237,7 +237,9 @@ feature %q{ visit '/admin/products/bulk_edit' - click_link 'New Product' + #save_screenshot "/Users/willmarshall/Desktop/foo.png" + #save_and_open_page + find("a", text: "NEW PRODUCT").click page.should have_content 'NEW PRODUCT' @@ -607,7 +609,7 @@ feature %q{ visit '/admin/products/bulk_edit' page.should have_selector "a.view-variants" - all("a.view-variants").each{ |e| e.click } + all("a.view-variants").each { |e| e.click } page.should have_selector "a.delete-variant", :count => 3 @@ -618,7 +620,7 @@ feature %q{ visit '/admin/products/bulk_edit' page.should have_selector "a.view-variants" - all("a.view-variants").select{ |e| e.visible? }.each{ |e| e.click } + all("a.view-variants").select { |e| e.visible? }.each { |e| e.click } page.should have_selector "a.delete-variant", :count => 2 end @@ -783,7 +785,7 @@ feature %q{ select '25', :from => 'perPage' page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page1product" }.should == true - click_link "2" + find("a", text: "2").click page.all("input[name='product_name']").select{ |e| e.visible? }.all?{ |e| e.value == "page2product" }.should == true end @@ -795,7 +797,7 @@ feature %q{ visit '/admin/products/bulk_edit' select '25', :from => 'perPage' - click_link "3" + find("a", text: "3").click select '50', :from => 'perPage' page.first("div.pagenav span.page.current").should have_text "2" page.all("input[name='product_name']", :visible => true).length.should == 1 @@ -869,7 +871,7 @@ feature %q{ describe "clicking the 'Remove Filter' link" do before(:each) do - click_link "Remove Filter" + find("a", text: "Remove Filter").click end it "removes the filter from the list of applied filters" do diff --git a/spec/features/admin/cms_spec.rb b/spec/features/admin/cms_spec.rb index c3b2fe3454..29fc2412e2 100644 --- a/spec/features/admin/cms_spec.rb +++ b/spec/features/admin/cms_spec.rb @@ -8,7 +8,6 @@ feature %q{ include AuthenticationWorkflow include WebHelper - scenario "admin can access CMS admin and return to Spree admin" do login_to_admin_section click_link 'Configuration' @@ -16,7 +15,7 @@ feature %q{ page.should have_content "ComfortableMexicanSofa" click_link 'Spree Admin' - current_path.should == spree.admin_path + current_path.should match(/^\/admin/) end scenario "anonymous user can't access CMS admin" do @@ -29,6 +28,6 @@ feature %q{ login_to_consumer_section visit cms_admin_path page.should_not have_content "ComfortableMexicanSofa" - page.should have_content "WHERE WOULD YOU LIKE TO SHOP?" + current_path.should == root_path end end diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index e26be25c1e..8f639d94fb 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -71,7 +71,7 @@ feature %q{ end scenario "manage products that I supply" do - visit 'admin/products' + visit '/admin/products' within '#listing_products' do page.should have_content 'Green eggs' @@ -90,12 +90,12 @@ feature %q{ end scenario "should not be able to see system configuration" do - visit 'admin/general_settings/edit' + visit '/admin/general_settings/edit' page.should have_content 'Authorization Failure' end scenario "should not be able to see user management" do - visit 'admin/users' + visit '/admin/users' page.should have_content 'Authorization Failure' end end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index fdd5c009d4..2eff8508c7 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -9,7 +9,7 @@ feature %q{ before :all do @orig_default_wait_time = Capybara.default_wait_time - Capybara.default_wait_time = 5 + Capybara.default_wait_time = 10 end after :all do @@ -217,11 +217,11 @@ feature %q{ end # And the distributors should have fees - distributor = oc.distributors.sort_by(&:name).first + distributor = oc.distributors.sort_by(&:id).first page.should have_select 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_id', selected: distributor.name page.should have_select 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_fee_id', selected: distributor.enterprise_fees.first.name - distributor = oc.distributors.sort_by(&:name).last + distributor = oc.distributors.sort_by(&:id).last page.should have_select 'order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_id', selected: distributor.name page.should have_select 'order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_fee_id', selected: distributor.enterprise_fees.first.name end @@ -295,6 +295,7 @@ feature %q{ click_button 'Add supplier' page.all("table.exchanges tr.supplier td.products input").each { |e| e.click } + wait_until { page.find("#order_cycle_incoming_exchange_1_variants_#{initial_variants.last.id}", visible: true).present? } page.find("#order_cycle_incoming_exchange_1_variants_#{initial_variants.last.id}", visible: true).click # uncheck (with visible:true filter) check "order_cycle_incoming_exchange_2_variants_#{v1.id}" check "order_cycle_incoming_exchange_2_variants_#{v2.id}" @@ -462,17 +463,22 @@ feature %q{ login_to_admin_as @new_user end - scenario "can view products I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { coordinator: supplier1, name: 'Order Cycle 1' } ) + scenario "viewing a list of order cycles I am coordinating" do + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) click_link "Order Cycles" + # I should see only the order cycle I am coordinating page.should have_content oc_user_coordinating.name page.should_not have_content oc_for_other_user.name + + # The order cycle should not show enterprises that I don't manage + page.should_not have_selector 'td.suppliers', text: supplier2.name + page.should_not have_selector 'td.distributors', text: distributor2.name end - scenario "can create a new order cycle" do + scenario "creating a new order cycle" do click_link "Order Cycles" click_link 'New Order Cycle' @@ -510,6 +516,16 @@ feature %q{ order_cycle.coordinator.should == distributor1 end + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier1, supplier2], coordinator: supplier1, distributors: [distributor1, distributor2], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # I should not see exchanges for supplier2 or distributor2 + page.all('tr.supplier').count.should == 1 + page.all('tr.distributor').count.should == 1 + end + scenario "cloning an order cycle" do oc = create(:simple_order_cycle) diff --git a/spec/features/chili/enterprises_distributor_info_rich_text_feature_spec.rb b/spec/features/chili/enterprises_distributor_info_rich_text_feature_spec.rb deleted file mode 100644 index 10214d69dc..0000000000 --- a/spec/features/chili/enterprises_distributor_info_rich_text_feature_spec.rb +++ /dev/null @@ -1,170 +0,0 @@ -require 'spec_helper' - -feature "enterprises distributor info as rich text" do - include AuthenticationWorkflow - include WebHelper - - before(:each) do - OpenFoodNetwork::FeatureToggle.stub(:features).and_return({eaterprises: false, - local_organics: true, - enterprises_distributor_info_rich_text: true}) - - - ActionMailer::Base.delivery_method = :test - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries = [] - # The deployment is not set to local_organics on Rails init, so these - # - # initializers won't run. Re-call them now that the deployment is set. - end - - after do - ActionMailer::Base.deliveries.clear - end - - - scenario "setting distributor info as admin" do - # Given I'm signed in as an admin - login_to_admin_section - - # When I go to create a new enterprise - click_link 'Enterprises' - click_link 'New Enterprise' - - # Then I should see fields 'Profile Info' and 'Distributor Info' - page.should have_content 'About Us' - page.should have_content 'How does it work' - - # When I fill out the form and create the enterprise - fill_in 'enterprise_name', :with => 'Eaterprises' - fill_in 'enterprise_long_description', with: 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' - fill_in 'enterprise_distributor_info', with: 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - fill_in 'enterprise_address_attributes_address1', with: '35 Ballantyne St' - fill_in 'enterprise_address_attributes_city', with: 'Thornbury' - fill_in 'enterprise_address_attributes_zipcode', with: '3072' - select 'Australia', from: 'enterprise_address_attributes_country_id' - select 'Victoria', from: 'enterprise_address_attributes_state_id' - - click_button 'Create' - - # Then I should see the enterprise details - flash_message.should == 'Enterprise "Eaterprises" has been successfully created!' - click_link 'Eaterprises' - page.should have_selector "tr[data-hook='long_description'] th", text: 'Profile Info:' - page.should have_selector "tr[data-hook='long_description'] td", text: 'Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.' - - page.should have_selector "tr[data-hook='distributor_info'] th", text: 'Distributor Info:' - page.should have_selector "tr[data-hook='distributor_info'] td", text: 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - end - - scenario "viewing distributor info with product distribution", js: true do - - d = create(:distributor_enterprise, distributor_info: 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.', next_collection_at: 'Thursday 2nd May') - p = create(:product, :distributors => [d]) - - setup_shipping_details d - - login_to_consumer_section - visit spree.select_distributor_order_path(d) - ActionMailer::Base.deliveries.clear - - # -- Product details page - visit spree.product_path p - within '#product-distributor-details' do - page.should have_content 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - page.should have_content 'Thursday 2nd May' - end - - # -- Checkout - click_button 'Add To Cart' - visit "/checkout" - within 'fieldset#shipping' do - page.should have_content 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - page.should have_content 'Thursday 2nd May' - end - - # -- Confirmation - complete_purchase_from_checkout_address_page - #page.should have_content 'Thursday 2nd May' - - # -- Purchase email - wait_until { ActionMailer::Base.deliveries.length == 1 } - email = ActionMailer::Base.deliveries.last - email.body.should =~ /Chu ge sai yubi dan bisento tobi ashi yubi ge omote./ - email.body.should =~ /Thursday 2nd May/ - end - - scenario "viewing distributor info with order cycle distribution", js: true do - set_feature_toggle :order_cycles, true - ActionMailer::Base.deliveries.clear - - d = create(:distributor_enterprise, name: 'Green Grass', distributor_info: 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.', next_collection_at: 'Thursday 2nd May') - create_enterprise_group_for d - p = create(:product) - oc = create(:simple_order_cycle, distributors: [d], variants: [p.master]) - ex = oc.exchanges.outgoing.last - ex = Exchange.find ex.id - ex.pickup_time = 'Friday 4th May' - ex.save! - - setup_shipping_details d - - login_to_consumer_section - ActionMailer::Base.deliveries.clear - click_link 'Green Grass' - visit enterprise_path d - - # -- Product details page - click_link p.name - within '#product-distributor-details' do - page.should have_content 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - page.should have_content 'Friday 4th May' - end - - # -- Checkout - click_button 'Add To Cart' - find('#checkout-link').click - visit "/checkout" - within 'fieldset#shipping' do - page.should have_content 'Chu ge sai yubi dan bisento tobi ashi yubi ge omote.' - page.should have_content 'Friday 4th May' - end - - # -- Confirmation - complete_purchase_from_checkout_address_page - page.should have_content 'Friday 4th May' - - # -- Purchase email - wait_until { ActionMailer::Base.deliveries.length == 1 } - email = ActionMailer::Base.deliveries.last - email.body.should =~ /Chu ge sai yubi dan bisento tobi ashi yubi ge omote./ - email.body.should =~ /Friday 4th May/ - end - - - private - def setup_shipping_details(distributor) - zone = create(:zone) - c = Spree::Country.find_by_name('Australia') - Spree::ZoneMember.create(:zoneable => c, :zone => zone) - create(:shipping_method, zone: zone, require_ship_address: false) - create(:payment_method, :description => 'Cheque payment method', distributors: [distributor]) - end - - - def complete_purchase_from_checkout_address_page - fill_in_fields('order_bill_address_attributes_firstname' => 'Joe', - 'order_bill_address_attributes_lastname' => 'Luck', - 'order_bill_address_attributes_address1' => '19 Sycamore Lane', - 'order_bill_address_attributes_city' => 'Horse Hill', - 'order_bill_address_attributes_zipcode' => '3213', - 'order_bill_address_attributes_phone' => '12999911111') - - select('Australia', :from => 'order_bill_address_attributes_country_id') - select('Victoria', :from => 'order_bill_address_attributes_state_id') - - click_checkout_continue_button - click_checkout_continue_button - click_checkout_continue_button - end -end diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/home_spec.rb new file mode 100644 index 0000000000..67599d97a0 --- /dev/null +++ b/spec/features/consumer/home_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +feature 'Home', js: true do + include AuthenticationWorkflow + include UIComponentHelper + + let(:distributor) { create(:distributor_enterprise) } + let(:d1) { create(:distributor_enterprise) } + let(:d2) { create(:distributor_enterprise) } + let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } + before do + distributor + order_cycle + visit "/" + end + + it "shows all hubs" do + page.should have_content distributor.name + expand_active_table_node distributor.name + page.should have_content "Shop at #{distributor.name}" + end + + it "should grey out hubs that are not in an order cycle" do + create(:simple_product, distributors: [d1, d2]) + visit root_path + page.should have_selector 'hub.inactive', text: d1.name + page.should have_selector 'hub.inactive', text: d2.name + end + + it "should link to the hub page" do + follow_active_table_node distributor.name + current_path.should == "/shop" + end +end diff --git a/spec/features/consumer/landing_page_spec.rb b/spec/features/consumer/landing_page_spec.rb index 5935f393b4..474e3ff922 100644 --- a/spec/features/consumer/landing_page_spec.rb +++ b/spec/features/consumer/landing_page_spec.rb @@ -13,36 +13,10 @@ feature %q{ scenario "viewing the landing page" do page.should have_selector "#suburb_search" - page.should have_selector 'a', :text => "Login" - page.should have_selector 'a', :text => "Sign Up" end - describe "login" do - before(:each) do - Spree::User.create(:email => "spree123@example.com", :password => "spree123") - find(:xpath, '//a[contains(text(), "Login")]').click - end - - scenario "with valid credentials" do - fill_in 'login_spree_user_email', :with => 'spree123@example.com' - fill_in 'login_spree_user_password', :with => 'spree123' - find(:xpath, '//input[contains(@value, "Login")][contains(@type, "submit")]').click - sleep 4 - page.should_not have_content("Invalid email or password") - page.should have_content("Sign Out") - end - - scenario "with invalid credentials" do - fill_in 'login_spree_user_email', :with => 'spree123@example.com.WRONG' - fill_in 'login_spree_user_password', :with => 'spree123_WRONG' - find(:xpath, '//input[contains(@value, "Login")][contains(@type, "submit")]').click - sleep 4 - page.should have_content("Invalid email or password") - page.should_not have_content("Sign Out") - end - end - - describe "suburb search" do + # PENDING - we're not using this anymore + pending "suburb search" do before(:each) do state_id_vic = Spree::State.where(abbr: "Vic").first.id Suburb.create(name: "Camberwell", postcode: 3124, latitude: -37.824818, longitude: 145.057957, state_id: state_id_vic) @@ -50,16 +24,12 @@ feature %q{ it "should auto complete suburbs" do suburb_search_field_id = "suburb_search" - fill_in suburb_search_field_id, :with => "Cambe" - page.execute_script %Q{ $('##{suburb_search_field_id}').trigger("focus") } page.execute_script %Q{ $('##{suburb_search_field_id}').trigger("keydown") } - sleep 1 - page.should have_content("Camberwell") page.should have_content("3124") end end -end \ No newline at end of file +end diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 605df207a9..c6ac18e87d 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -4,6 +4,7 @@ feature "As a consumer I want to check out my cart", js: true do include AuthenticationWorkflow include WebHelper include ShopWorkflow + include UIComponentHelper let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } @@ -12,13 +13,14 @@ feature "As a consumer I want to check out my cart", js: true do let(:order) { Spree::Order.last } before do + order_cycle create_enterprise_group_for distributor end # This was refactored in the new checkout # We have monkey-patched in some of the new features # Test suite works in that branch - pending "Login behaviour" do + describe "Login behaviour" do let(:user) { create_enterprise_user } before do select_distributor @@ -46,7 +48,7 @@ feature "As a consumer I want to check out my cart", js: true do it "renders the login form if user is logged out" do within "section[role='main']" do - page.should have_content "USER" + page.should have_content "User" end end end diff --git a/spec/features/consumer/shopping/checkout_plumbing_spec.rb b/spec/features/consumer/shopping/checkout_plumbing_spec.rb deleted file mode 100644 index 2295b07146..0000000000 --- a/spec/features/consumer/shopping/checkout_plumbing_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - - -feature "As a consumer I want to check out my cart", js: true do - include AuthenticationWorkflow - include ShopWorkflow - include WebHelper - - let(:distributor) { create(:distributor_enterprise) } - let(:supplier) { create(:supplier_enterprise) } - let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } - let(:product) { create(:simple_product, supplier: supplier) } - let(:order) { Spree::Order.last } - - before do - create_enterprise_group_for distributor - end - describe "Attempting to access checkout without meeting the preconditions" do - it "redirects to the homepage if no distributor is selected" do - visit "/shop/checkout" - current_path.should == root_path - end - - it "redirects to the shop page if we have a distributor but no order cycle selected" do - select_distributor - visit "/shop/checkout" - current_path.should == shop_path - end - - it "redirects to the shop page if the current order is empty" do - select_distributor - select_order_cycle - visit "/shop/checkout" - current_path.should == shop_path - end - - it "renders checkout if we have distributor and order cycle selected" do - select_distributor - select_order_cycle - add_product_to_cart - visit "/shop/checkout" - current_path.should == "/shop/checkout" - end - end -end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 7cf28675e1..a780e9fd4e 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -5,6 +5,7 @@ feature "As a consumer I want to check out my cart", js: true do include AuthenticationWorkflow include ShopWorkflow include WebHelper + include UIComponentHelper let(:distributor) { create(:distributor_enterprise) } let(:supplier) { create(:supplier_enterprise) } @@ -13,139 +14,119 @@ feature "As a consumer I want to check out my cart", js: true do let(:order) { Spree::Order.last } before do + order_cycle # force this to load create_enterprise_group_for distributor end - # Disabled :in for performance reasons - [:out].each do |auth_state| - describe "logged #{auth_state.to_s}, distributor selected, order cycle selected, product in cart" do - let(:user) { create_enterprise_user } + describe "logged out, distributor selected, order cycle selected, product in cart" do + let(:user) { create_enterprise_user } + before do + select_distributor + select_order_cycle + add_product_to_cart + end + + describe "with shipping methods" do + let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow") } + let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue") } before do - if auth_state == :in - login_to_consumer_section - end - select_distributor - select_order_cycle - add_product_to_cart + distributor.shipping_methods << sm1 + distributor.shipping_methods << sm2 end - describe "with shipping methods" do - let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow") } - let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue") } + context "on the checkout page" do before do - distributor.shipping_methods << sm1 - distributor.shipping_methods << sm2 visit "/shop/checkout" end it "shows all shipping methods, but doesn't show ship address when not needed" do + toggle_accordion "Shipping" page.should have_content "Frogs" page.should have_content "Donkeys" - choose(sm2.name) - find("#ship_address", visible: false).visible?.should be_false end context "When shipping method requires an address" do before do + toggle_accordion "Shipping" choose(sm1.name) end - it "shows the hidden ship address fields by default" do - check "Shipping address same as billing address?" - find("#ship_address_hidden").visible?.should be_true - find("#ship_address > div.visible", visible: false).visible?.should be_false - - # Check it keeps state - click_button "Purchase" - find_field("Shipping address same as billing address?").should be_checked - end - it "shows ship address forms when 'same as billing address' is unchecked" do uncheck "Shipping address same as billing address?" - find("#ship_address_hidden", visible: false).visible?.should be_false find("#ship_address > div.visible").visible?.should be_true + end + end + end - # Check it keeps state + describe "with payment methods" do + let(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } + let(:pm2) { create(:payment_method, distributors: [distributor]) } + let(:pm3) { create(:payment_method, distributors: [distributor], name: "Paypal", type: "Spree::BillingIntegration::PaypalExpress") } + + before do + pm1 # Lazy evaluation of ze create()s + pm2 + visit "/shop/checkout" + toggle_accordion "Payment Details" + end + + it "shows all available payment methods" do + page.should have_content pm1.name + page.should have_content pm2.name + end + + describe "Purchasing" do + it "takes us to the order confirmation page when we submit a complete form" do + toggle_accordion "Shipping" + choose sm2.name + toggle_accordion "Payment Details" + choose pm1.name + toggle_accordion "Customer Details" + within "#details" do + fill_in "First Name", with: "Will" + fill_in "Last Name", with: "Marshall" + fill_in "Email", with: "test@test.com" + fill_in "Phone", with: "0468363090" + end + toggle_accordion "Billing" + within "#billing" do + fill_in "Address", with: "123 Your Face" + select "Australia", from: "Country" + select "Victoria", from: "State" + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + end click_button "Purchase" - find_field("Shipping address same as billing address?").should_not be_checked - end - end - - it "copies billing address to hidden shipping address fields" do - choose(sm1.name) - check "Shipping address same as billing address?" - within "#billing" do - fill_in "Address", with: "testy" - end - within "#ship_address_hidden" do - find("#order_ship_address_attributes_address1", visible: false).value.should == "testy" - end - end - - describe "with payment methods" do - let(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } - let(:pm2) { create(:payment_method, distributors: [distributor]) } - let(:pm3) { create(:payment_method, distributors: [distributor], name: "Paypal", type: "Spree::BillingIntegration::PaypalExpress") } - - before do - pm1 # Lazy evaluation of ze create()s - pm2 - visit "/shop/checkout" + sleep 10 + page.should have_content "Your order has been processed successfully" end - it "shows all available payment methods" do - page.should have_content pm1.name - page.should have_content pm2.name - end - - describe "Purchasing" do - it "re-renders with errors when we submit the incomplete form" do - choose sm2.name - click_button "Purchase" - current_path.should == "/shop/checkout" - page.should have_content "can't be blank" + it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do + toggle_accordion "Shipping" + choose sm1.name + toggle_accordion "Payment Details" + choose pm1.name + toggle_accordion "Customer Details" + within "#details" do + fill_in "First Name", with: "Will" + fill_in "Last Name", with: "Marshall" + fill_in "Email", with: "test@test.com" + fill_in "Phone", with: "0468363090" end - - it "renders errors on the shipping method where appropriate" - - it "takes us to the order confirmation page when we submit a complete form" do - choose sm2.name - choose pm1.name - within "#details" do - fill_in "First Name", with: "Will" - fill_in "Last Name", with: "Marshall" - fill_in "Address", with: "123 Your Face" - select "Australia", from: "Country" - select "Victoria", from: "State" - fill_in "Customer E-Mail", with: "test@test.com" - fill_in "Phone", with: "0468363090" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - end - click_button "Purchase" - page.should have_content "Your order has been processed successfully" - end - - it "takes us to the order confirmation page when submitted with 'same as billing address' checked" do - choose sm1.name - choose pm1.name - within "#details" do - fill_in "First Name", with: "Will" - fill_in "Last Name", with: "Marshall" - fill_in "Address", with: "123 Your Face" - select "Australia", from: "Country" - select "Victoria", from: "State" - fill_in "Customer E-Mail", with: "test@test.com" - fill_in "Phone", with: "0468363090" - fill_in "City", with: "Melbourne" - fill_in "Postcode", with: "3066" - end - check "Shipping address same as billing address?" - click_button "Purchase" - page.should have_content "Your order has been processed successfully" + toggle_accordion "Billing" + within "#billing" do + fill_in "City", with: "Melbourne" + fill_in "Postcode", with: "3066" + fill_in "Address", with: "123 Your Face" + select "Australia", from: "Country" + select "Victoria", from: "State" end + toggle_accordion "Shipping" + check "Shipping address same as billing address?" + click_button "Purchase" + sleep 10 + page.should have_content "Your order has been processed successfully" end end end end - end end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 303d5de197..4a91b1186a 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -3,14 +3,20 @@ require 'spec_helper' feature "As a consumer I want to shop with a distributor", js: true do include AuthenticationWorkflow include WebHelper + include UIComponentHelper describe "Viewing a distributor" do + let(:supplier) { create(:supplier_enterprise) } let(:distributor) { create(:distributor_enterprise) } + let(:oc1) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 2.days.from_now) } + let(:oc2) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 3.days.from_now)} + let(:exchange2) { Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) } - before do #temporarily using the old way to select distributor + before do + oc1 create_enterprise_group_for distributor - visit "/" - click_link distributor.name + visit root_path + follow_active_table_node distributor.name end it "shows a distributor with images" do @@ -23,12 +29,9 @@ feature "As a consumer I want to shop with a distributor", js: true do end describe "with products in order cycles" do - let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:product, supplier: supplier) } - let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } - before do - exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id) + exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) exchange.variants << product.master end @@ -39,13 +42,11 @@ feature "As a consumer I want to shop with a distributor", js: true do end end - describe "selecting an order cycle" do - let(:oc1) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 2.days.from_now)} - let(:oc2) {create(:simple_order_cycle, distributors: [distributor], orders_close_at: 3.days.from_now)} + # PENDING THIS because Capybara is the wrong tool to test Angular and these tests keep breaking + pending "selecting an order cycle" do let(:exchange1) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } - let(:exchange2) { Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) } + it "selects an order cycle if only one is open" do - # create order cycle exchange1.update_attribute :pickup_time, "turtles" visit shop_path page.should have_selector "option[selected]", text: 'turtles' @@ -61,6 +62,7 @@ feature "As a consumer I want to shop with a distributor", js: true do it "shows a select with all order cycles, but doesn't show the products by default" do page.should have_selector "option", text: 'frogs' page.should have_selector "option", text: 'turtles' + page.should_not have_selector "option[selected]" page.should_not have_selector("input.button.right", visible: true) end diff --git a/spec/features/consumer/temp_landing_page_spec.rb b/spec/features/consumer/temp_landing_page_spec.rb deleted file mode 100644 index 183284514f..0000000000 --- a/spec/features/consumer/temp_landing_page_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'spec_helper' - -feature %q{ - As a consumer - I want to see the landing page - So I can choose a distributor -}, js: true do - include AuthenticationWorkflow - - - let(:d1) { create(:distributor_enterprise, name: 'Murandaka') } - let(:d2) { create(:distributor_enterprise, name: 'Ballantyne') } - let(:d3) { create(:distributor_enterprise, name: "O'Hea Street") } - let(:d4) { create(:distributor_enterprise, name: "PepperTree Place") } - - let!(:eg1) { create(:enterprise_group, name: 'Group One', - on_front_page: true, enterprises: [d1, d2]) } - let!(:eg2) { create(:enterprise_group, name: 'Group Two', - on_front_page: true, enterprises: [d3, d4]) } - - background do - visit root_path - end - - describe "static content" do - it "should have a logo" do - page.should have_xpath("//img[@src=\"/assets/ofn_logo_black.png\"]") - end - - it "should have explanatory text" do - page.should have_content("WHERE WOULD YOU LIKE TO SHOP?") - end - end - - describe "account links" do - it "should display log in and sign up links when signed out" do - page.should have_link 'Login' - page.should have_link 'Sign Up' - end - - it "should not display links when signed in" do - login_to_consumer_section - visit root_path - - #page.should_not have_link 'Login' - page.should_not have_selector('#sidebarLoginButton', visible: true) - page.should_not have_selector('#sidebarSignUpButton', visible: true) - #page.should_not have_link 'Sign Up' - end - end - - describe "hub list" do - it "should display grouped hubs" do - page.should have_content 'GROUP ONE' - page.should have_link 'Murandaka' - page.should have_link 'Ballantyne' - - page.should have_content 'GROUP TWO' - page.should have_link "O'Hea Street" - page.should have_link "PepperTree Place" - end - - it "should grey out hubs that are not in an order cycle" do - create(:simple_order_cycle, distributors: [d1, d3]) - create(:simple_product, distributors: [d1, d2]) - - visit root_path - - page.should have_selector 'a.shop-distributor.active', text: 'Murandaka' - page.should have_selector 'a.shop-distributor.inactive', text: 'Ballantyne' - page.should have_selector 'a.shop-distributor.active', text: "O'Hea Street" - page.should have_selector 'a.shop-distributor.inactive', text: 'PepperTree Place' - end - - it "should link to the hub page" do - click_on 'Murandaka' - current_path.should == "/shop" - end - end - -end diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb new file mode 100644 index 0000000000..6563ed8af9 --- /dev/null +++ b/spec/helpers/checkout_helper_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + + +describe CheckoutHelper do + it "generates html for validated inputs" do + helper.should_receive(:render).with( + partial: "shared/validated_input", + locals: {name: "test", path: "foo", + attributes: {:required=>true, :type=>:email, :name=>"foo", :id=>"foo", "ng-model"=>"foo", "ng-class"=>"{error: !fieldValid('foo')}"}} + ) + + helper.validated_input("test", "foo", type: :email) + end +end diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js new file mode 100644 index 0000000000..9e821754de --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,10 @@ +//= require angular +//= require angular-resource +//= require angular-animate +//= require angular-mocks +//= require angular-cookies +//= require angular-backstretch.js +//= require angular-flash.min.js +//= require moment + +angular.module('templates', []) diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 1f6ec2a0da..73dc80c13c 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -18,30 +18,29 @@ describe "AdminOrderMgmtCtrl", -> returnedDistributors = ["list of distributors"] returnedOrderCycles = [ "oc1", "oc2", "oc3" ] httpBackend.expectGET("/api/users/authorise_api?token=api_key").respond success: "Use of API Authorised" - httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").respond returnedSuppliers - httpBackend.expectGET("/api/enterprises/managed?template=bulk_index&q[is_distributor_eq]=true").respond returnedDistributors - httpBackend.expectGET("/api/order_cycles/managed").respond returnedOrderCycles + httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").respond returnedSuppliers + httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[is_distributor_eq]=true").respond returnedDistributors + httpBackend.expectGET("/api/order_cycles/accessible").respond returnedOrderCycles spyOn(scope, "initialiseVariables").andCallThrough() spyOn(scope, "fetchOrders").andReturn "nothing" - spyOn(returnedSuppliers, "unshift") - spyOn(returnedDistributors, "unshift") - spyOn(returnedOrderCycles, "unshift") + #spyOn(returnedSuppliers, "unshift") + #spyOn(returnedDistributors, "unshift") + #spyOn(returnedOrderCycles, "unshift") scope.initialise "api_key" httpBackend.flush() - expect(scope.suppliers).toEqual ["list of suppliers"] - expect(scope.distributors).toEqual ["list of distributors"] - expect(scope.orderCycles).toEqual [ "oc1", "oc2", "oc3" ] - expect(scope.initialiseVariables.calls.length).toEqual 1 - expect(scope.fetchOrders.calls.length).toEqual 1 - expect(returnedSuppliers.unshift.calls.length).toEqual 1 - expect(returnedDistributors.unshift.calls.length).toEqual 1 - expect(returnedOrderCycles.unshift.calls.length).toEqual 1 - expect(scope.spree_api_key_ok).toEqual true + + expect(scope.suppliers).toEqual [{ id : '', name : 'All' }, 'list of suppliers'] + expect(scope.distributors).toEqual [ { id : '', name : 'All' }, 'list of distributors' ] + expect(scope.orderCycles).toEqual [ { id : '', name : 'All' }, 'oc1', 'oc2', 'oc3' ] + + expect(scope.initialiseVariables.calls.length).toBe 1 + expect(scope.fetchOrders.calls.length).toBe 1 + expect(scope.spree_api_key_ok).toBe true describe "fetching orders", -> beforeEach -> scope.initialiseVariables() - httpBackend.expectGET("/api/orders/managed?template=bulk_index&q[completed_at_not_null]=true&q[completed_at_gt]=SomeDate&q[completed_at_lt]=SomeDate").respond "list of orders" + httpBackend.expectGET("/api/orders/managed?template=bulk_index;page=1;per_page=500;q[completed_at_not_null]=true;q[completed_at_gt]=SomeDate;q[completed_at_lt]=SomeDate").respond "list of orders" it "makes a call to dataFetcher, with current start and end date parameters", -> scope.fetchOrders() @@ -625,4 +624,4 @@ describe "Auxiliary functions", -> expect(formatDate(date)).toEqual "2010-06-15" it "returns a time formatted as hh-MM:ss", -> - expect(formatTime(date)).toEqual "05:10:30" \ No newline at end of file + expect(formatTime(date)).toEqual "05:10:30" diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 4621f9a5c7..942c2f109a 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -1100,7 +1100,7 @@ describe "AdminProductEditCtrl", -> describe "when the variant has been saved", -> - it "deletes variants with a http delete request to /api/products/product_id/variants/(variant_id)", -> + it "deletes variants with a http delete request to /api/products/product_permalink/variants/(variant_id)", -> spyOn(window, "confirm").andReturn true scope.products = [ { @@ -1117,7 +1117,7 @@ describe "AdminProductEditCtrl", -> } ] scope.dirtyProducts = {} - httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data" + httpBackend.expectDELETE("/api/products/apples/variants/3/soft_delete").respond 200, "data" scope.deleteVariant scope.products[0], scope.products[0].variants[0] httpBackend.flush() @@ -1159,7 +1159,7 @@ describe "AdminProductEditCtrl", -> id: 13 name: "P1" - httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data" + httpBackend.expectDELETE("/api/products/apples/variants/3/soft_delete").respond 200, "data" scope.deleteVariant scope.products[0], scope.products[0].variants[0] httpBackend.flush() expect(scope.products[0].variants).toEqual [ @@ -1434,4 +1434,4 @@ describe "Taxons service", -> $httpBackend.expectGET("/admin/taxons/search?q=lala").respond 200, response taxons = Taxons.findByTerm("lala") $httpBackend.flush() - expect(angular.equals(taxons,response)).toBe true \ No newline at end of file + expect(angular.equals(taxons,response)).toBe true diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee new file mode 100644 index 0000000000..b2f4005199 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/details_controller_spec.js.coffee @@ -0,0 +1,35 @@ +describe "DetailsCtrl", -> + ctrl = null + scope = null + order = null + + beforeEach -> + module("Darkswarm") + inject ($controller, $rootScope) -> + scope = $rootScope.$new() + ctrl = $controller 'DetailsCtrl', {$scope: scope} + + + it "finds a field by path", -> + scope.details = + path: "test" + expect(scope.field('path')).toEqual "test" + + it "tests validity", -> + scope.details = + path: + $dirty: true + $invalid: true + expect(scope.fieldValid('path')).toEqual false + + it "returns errors by path", -> + scope.Order = + errors: -> + scope.details = + path: + $error: + email: true + required: true + expect(scope.fieldErrors('path')).toEqual ["must be email address", "can't be blank"].join ", " + + diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee index fae814e4e8..11cb4b7f3c 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout_controller_spec.js.coffee @@ -1,34 +1,28 @@ describe "CheckoutCtrl", -> ctrl = null scope = null - order = null + Order = null beforeEach -> module("Darkswarm") - order = - id: 3102 - shipping_method_id: "7" - ship_address_same_as_billing: true - payment_method_id: null - shipping_methods: - 7: - require_ship_address: true - price: 0.0 - - 25: - require_ship_address: false - price: 13 - inject ($controller) -> - scope = {} - ctrl = $controller 'CheckoutCtrl', {$scope: scope, order: order} - - - it 'Gets the ship address automatically', -> - expect(scope.require_ship_address).toEqual true - - it 'Gets the current shipping price', -> - expect(scope.shippingPrice()).toEqual 0.0 - scope.order.shipping_method_id = 25 - expect(scope.shippingPrice()).toEqual 13 - + angular.module('Darkswarm').value('user', {}) + Order = { + submit: -> + navigate: -> + order: + id: 1 + } + inject ($controller, $rootScope) -> + scope = $rootScope.$new() + ctrl = $controller 'CheckoutCtrl', {$scope: scope, Order: Order} + it "defaults the user accordion to visible", -> + expect(scope.accordion.user).toEqual true + + it "delegates to the service on submit", -> + event = { + preventDefault: -> + } + spyOn(Order, "submit") + scope.purchase(event) + expect(Order.submit).toHaveBeenCalled() diff --git a/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee new file mode 100644 index 0000000000..a2d14f5811 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee @@ -0,0 +1,32 @@ +describe "HubNodeCtrl", -> + ctrl = null + scope = null + hub = null + CurrentHub = null + + beforeEach -> + module 'Darkswarm' + scope = + hub: {} + CurrentHub = + id: 99 + + inject ($controller, $location)-> + ctrl = $controller 'HubNodeCtrl', {$scope: scope, CurrentHub: CurrentHub, $location : $location} + + it "knows whether the controlled hub is current", -> + scope.hub = {id: 1} + expect(scope.current()).toEqual false + scope.hub = {id: 99} + expect(scope.current()).toEqual true + + it "knows whether selecting this hub will empty the cart", -> + CurrentHub.id = undefined + expect(scope.emptiesCart()).toEqual false + + CurrentHub.id = 99 + scope.hub.id = 99 + expect(scope.emptiesCart()).toEqual false + + scope.hub.id = 1 + expect(scope.emptiesCart()).toEqual true diff --git a/spec/javascripts/unit/darkswarm/controllers/ordercycle_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/ordercycle_controller_spec.js.coffee index 260f51982a..0eec42671c 100644 --- a/spec/javascripts/unit/darkswarm/controllers/ordercycle_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/ordercycle_controller_spec.js.coffee @@ -14,5 +14,5 @@ describe 'OrderCycleCtrl', -> scope = {} ctrl = $controller 'OrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle} - it "puts the order cycle in scope", -> - expect(scope.order_cycle).toEqual "test" + #it "puts the order cycle in scope", -> + #expect(scope.order_cycle).toEqual "test" diff --git a/spec/javascripts/unit/darkswarm/controllers/sidebar_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/sidebar_controller_spec.js.coffee index 0d051a8cfa..c1b4095564 100644 --- a/spec/javascripts/unit/darkswarm/controllers/sidebar_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/sidebar_controller_spec.js.coffee @@ -12,11 +12,3 @@ describe "SidebarCtrl", -> scope = $rootScope ctrl = $controller 'SidebarCtrl', {$scope: scope, $location: location} scope.$apply() - - it 'is active when a location is set', -> - expect(scope.active()).toEqual true - - it 'is inactive no location is set', -> - location.path = -> - null - expect(scope.active()).toEqual false diff --git a/spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee similarity index 95% rename from spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee rename to spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee index 9687e72380..f7a65867e8 100644 --- a/spec/javascripts/unit/darkswarm/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee @@ -39,6 +39,6 @@ describe 'OrderCycle service', -> it "tells us when no order cycle is selected", -> OrderCycle.order_cycle = null expect(OrderCycle.selected()).toEqual false - OrderCycle.order_cycle = {test: "blah"} + OrderCycle.order_cycle = {orders_close_at: "10 days ago"} expect(OrderCycle.selected()).toEqual true diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee new file mode 100644 index 0000000000..a6e3ab9642 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -0,0 +1,88 @@ +describe 'Order service', -> + Order = null + orderData = null + $httpBackend = null + CheckoutFormState = null + Navigation = null + flash = null + + beforeEach -> + orderData = { + id: 3102 + payment_method_id: null + bill_address: {test: "foo"} + ship_address: {test: "bar"} + shipping_methods: + 7: + require_ship_address: true + price: 0.0 + + 25: + require_ship_address: false + price: 13 + payment_methods: + 99: + test: "foo" + } + angular.module('Darkswarm').value('order', orderData) + module 'Darkswarm' + inject ($injector, _$httpBackend_)-> + $httpBackend = _$httpBackend_ + Order = $injector.get("Order") + Navigation = $injector.get("Navigation") + flash = $injector.get("flash") + CheckoutFormState = $injector.get("CheckoutFormState") + spyOn(Navigation, "go") # Stubbing out writes to window.location + + it "defaults the shipping method to the first", -> + expect(Order.order.shipping_method_id).toEqual 7 + expect(Order.shippingMethod()).toEqual { require_ship_address : true, price : 0 } + + # This is now handled via localStorage defaults + xit "defaults to 'same as billing' for address", -> + expect(Order.order.ship_address_same_as_billing).toEqual true + + it 'Tracks whether a ship address is required', -> + expect(Order.requireShipAddress()).toEqual true + Order.order.shipping_method_id = 25 + expect(Order.requireShipAddress()).toEqual false + + it 'Gets the current shipping price', -> + expect(Order.shippingPrice()).toEqual 0.0 + Order.order.shipping_method_id = 25 + expect(Order.shippingPrice()).toEqual 13 + + it 'Gets the current payment method', -> + expect(Order.paymentMethod()).toEqual null + Order.order.payment_method_id = 99 + expect(Order.paymentMethod()).toEqual {test: "foo"} + + it "Posts the Order to the server", -> + $httpBackend.expectPUT("/shop/checkout", {order: Order.preprocess()}).respond 200, {path: "test"} + Order.submit() + $httpBackend.flush() + + it "sends flash messages to the flash service", -> + $httpBackend.expectPUT("/shop/checkout").respond 400, {flash: {error: "frogs"}} + Order.submit() + $httpBackend.flush() + expect(flash.error).toEqual "frogs" + + it "puts errors into the scope", -> + $httpBackend.expectPUT("/shop/checkout").respond 400, {errors: {error: "frogs"}} + Order.submit() + $httpBackend.flush() + expect(Order.errors).toEqual {error: "frogs"} + + + it "Munges the order attributes to add _attributes as Rails needs", -> + expect(Order.preprocess().bill_address_attributes).not.toBe(undefined) + expect(Order.preprocess().bill_address).toBe(undefined) + expect(Order.preprocess().ship_address_attributes).not.toBe(undefined) + expect(Order.preprocess().ship_address).toBe(undefined) + + it "Munges the order attributes to clone ship address from bill address", -> + CheckoutFormState.ship_address_same_as_billing = false + expect(Order.preprocess().ship_address_attributes).toEqual(orderData.ship_address) + CheckoutFormState.ship_address_same_as_billing = true + expect(Order.preprocess().ship_address_attributes).toEqual(orderData.bill_address) diff --git a/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee new file mode 100644 index 0000000000..8551113280 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee @@ -0,0 +1,37 @@ +describe "Sidebar", -> + location = null + Sidebar = null + Navigation = null + + beforeEach -> + module("Darkswarm") + inject (_Sidebar_, $location, _Navigation_) -> + Sidebar = _Sidebar_ + Navigation = _Navigation_ + location = $location + Sidebar.paths = ["/test", "/frogs"] + + + it 'is active when a location in paths is set', -> + spyOn(location, "path").andReturn "/test" + expect(Sidebar.active()).toEqual true + + it 'is inactive if location is set', -> + spyOn(location, "path").andReturn null + expect(Sidebar.active()).toEqual false + + describe "Toggling on/off", -> + it 'toggles the current sidebar path', -> + expect(Sidebar.active()).toEqual false + Navigation.path = "/frogs" + Sidebar.toggle() + expect(Sidebar.active()).toEqual true + + it 'If current navigation path is not in the sidebar, it toggles the first sidebar path', -> + Navigation.path = "/donkeys" + spyOn(Navigation, 'navigate') + Sidebar.toggle() + expect(Navigation.navigate).toHaveBeenCalledWith("/test") + + + diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index d6d0ac882b..172b2cdb1a 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -401,10 +401,12 @@ describe 'OrderCycle services', -> it 'loads enterprise fees', -> enterprise_fees = EnterpriseFee.index() $httpBackend.flush() - expect(enterprise_fees).toEqual [ + expected_fees = [ new EnterpriseFee.EnterpriseFee({id: 1, name: "Yayfee", enterprise_id: 1}) new EnterpriseFee.EnterpriseFee({id: 2, name: "FeeTwo", enterprise_id: 2}) ] + for fee, i in enterprise_fees + expect(fee.id).toEqual(expected_fees[i].id) it 'reports its loadedness', -> expect(EnterpriseFee.loaded).toBe(false) @@ -828,4 +830,4 @@ describe 'OrderCycle services', -> expect(order_cycle.outgoing_exchanges[0].enterprise_fees).toEqual [{id: 3}, {id: 4}] expect(order_cycle.incoming_exchanges[0].enterprise_fee_ids).toBeUndefined() expect(order_cycle.outgoing_exchanges[0].enterprise_fee_ids).toBeUndefined() - \ No newline at end of file + diff --git a/spec/models/enterprise_fee_spec.rb b/spec/models/enterprise_fee_spec.rb index 806eb7759c..fc72f383d2 100644 --- a/spec/models/enterprise_fee_spec.rb +++ b/spec/models/enterprise_fee_spec.rb @@ -9,6 +9,25 @@ describe EnterpriseFee do it { should validate_presence_of(:name) } end + describe "callbacks" do + it "removes itself from order cycle coordinator fees when destroyed" do + ef = create(:enterprise_fee) + oc = create(:simple_order_cycle, coordinator_fees: [ef]) + + ef.destroy + oc.reload.coordinator_fee_ids.should be_empty + end + + it "removes itself from order cycle exchange fees when destroyed" do + ef = create(:enterprise_fee) + oc = create(:simple_order_cycle) + ex = create(:exchange, order_cycle: oc, enterprise_fees: [ef]) + + ef.destroy + ex.reload.exchange_fee_ids.should be_empty + end + end + describe "scopes" do describe "finding per-item enterprise fees" do it "does not return fees with FlatRate and FlexiRate calculators" do diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 5288101752..6fc1913a8f 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -47,6 +47,14 @@ describe Enterprise do end describe "scopes" do + + describe 'active' do + it 'find active enterprises' do + d1 = create(:distributor_enterprise, visible: false) + s1 = create(:supplier_enterprise) + Enterprise.visible.should == [s1] + end + end describe "distributors_with_active_order_cycles" do it "finds active distributors by order cycles" do @@ -254,6 +262,35 @@ describe Enterprise do enterprises.should include e2 end end + + describe "accessible_by" do + it "shows only enterprises that are invloved in order cycles which are common to those managed by the given user" do + user = create(:user) + user.spree_roles = [] + e1 = create(:enterprise) + e2 = create(:enterprise) + e3 = create(:enterprise) + e4 = create(:enterprise) + e1.enterprise_roles.build(user: user).save + oc = create(:simple_order_cycle, coordinator: e2, suppliers: [e1], distributors: [e3]) + + enterprises = Enterprise.accessible_by user + enterprises.length.should == 3 + enterprises.should include e1, e2, e3 + enterprises.should_not include e4 + end + + it "shows all enterprises for admin user" do + user = create(:admin_user) + e1 = create(:enterprise) + e2 = create(:enterprise) + + enterprises = Enterprise.managed_by user + enterprises.length.should == 2 + enterprises.should include e1 + enterprises.should include e2 + end + end end describe "has_supplied_products_on_hand?" do @@ -354,4 +391,17 @@ describe Enterprise do Enterprise.find_near(@suburb_in_nsw).count.should eql(0) end end + + describe "taxons" do + let(:distributor) { create(:distributor_enterprise) } + let(:taxon1) { create(:taxon) } + let(:taxon2) { create(:taxon) } + let(:product1) { create(:simple_product, taxons: [taxon1]) } + let(:product2) { create(:simple_product, taxons: [taxon1, taxon2]) } + + it "gets all taxons of all products" do + Spree::Product.stub(:in_distributor).and_return [product1, product2] + distributor.taxons.should == [taxon1, taxon2] + end + end end diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index 9614a4d357..ed6cdbfc93 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -85,6 +85,43 @@ describe Exchange do let(:distributor) { create(:distributor_enterprise) } let(:oc) { create(:simple_order_cycle, coordinator: coordinator) } + describe "finding exchanges managed by a particular user" do + let(:user) do + user = create(:user) + user.spree_roles = [] + user + end + + before { Exchange.destroy_all } + + it "returns exchanges where the user manages both the sender and the receiver" do + exchange = create(:exchange, order_cycle: oc) + exchange.sender.users << user + exchange.receiver.users << user + + Exchange.managed_by(user).should == [exchange] + end + + it "does not return exchanges where the user manages only the sender" do + exchange = create(:exchange, order_cycle: oc) + exchange.sender.users << user + + Exchange.managed_by(user).should be_empty + end + + it "does not return exchanges where the user manages only the receiver" do + exchange = create(:exchange, order_cycle: oc) + exchange.receiver.users << user + + Exchange.managed_by(user).should be_empty + end + + it "does not return exchanges where the user manages neither enterprise" do + exchange = create(:exchange, order_cycle: oc) + Exchange.managed_by(user).should be_empty + end + end + it "finds exchanges in a particular order cycle" do ex = create(:exchange, order_cycle: oc) Exchange.in_order_cycle(oc).should == [ex] diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index cb2da1e0b4..e4f979dc5b 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -494,4 +494,13 @@ describe OrderCycle do OrderCycle.first_opening_for(distributor).should == nil end end + + describe "finding open order cycles" do + it "should give the soonest closing order cycle for a distributor" do + distributor = create(:distributor_enterprise) + oc = create(:simple_order_cycle, name: 'oc 1', distributors: [distributor], orders_open_at: 1.days.ago, orders_close_at: 11.days.from_now) + oc2 = create(:simple_order_cycle, name: 'oc 2', distributors: [distributor], orders_open_at: 2.days.ago, orders_close_at: 12.days.from_now) + OrderCycle.first_closing_for(distributor).should == oc + end + end end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 71160c7f87..fc13925bae 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -39,12 +39,21 @@ module Spree should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) end + it "should not be able to access admin actions on orders" do + should_not have_ability([:admin], for: Spree::Order) + end + it "should be able to create a new product" do should have_ability(:create, for: Spree::Product) end it "should be able to read/write their enterprises' product variants" do - should have_ability([:admin, :index, :read, :create, :edit, :search, :update, :destroy], for: Spree::Variant) + should have_ability([:create], for: Spree::Variant) + should have_ability([:admin, :index, :read, :create, :edit, :search, :update, :destroy], for: p1.master) + end + + it "should not be able to read/write other enterprises' product variants" do + should_not have_ability([:admin, :index, :read, :create, :edit, :search, :update, :destroy], for: p2.master) end it "should be able to read/write their enterprises' product properties" do diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 9c37663e1e..4b9e1fab0a 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -374,4 +374,14 @@ module Spree end end end + + describe "destruction" do + it "destroys exchange variants" do + v = create(:variant) + e = create(:exchange, variants: [v]) + + v.destroy + e.reload.variant_ids.should be_empty + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 76d72d7291..6cf9229252 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,6 +23,9 @@ WebMock.disable_net_connect!(:allow_localhost => true) Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} require 'spree/core/testing_support/controller_requests' require 'spree/core/testing_support/capybara_ext' +require 'spree/api/testing_support/setup' +require 'spree/api/testing_support/helpers' +require 'spree/api/testing_support/helpers_decorator' require 'active_record/fixtures' fixtures_dir = File.expand_path('../../db/default', __FILE__) @@ -92,6 +95,8 @@ RSpec.configure do |config| config.include Spree::CheckoutHelpers config.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller config.include Devise::TestHelpers, :type => :controller + config.extend Spree::Api::TestingSupport::Setup, :type => :controller + config.include Spree::Api::TestingSupport::Helpers, :type => :controller config.include OpenFoodNetwork::FeatureToggleHelper config.include OpenFoodNetwork::EnterpriseGroupsHelper config.include OpenFoodNetwork::DistributionHelper diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 1473afe477..2f9bd3c5ff 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -2,7 +2,7 @@ module ShopWorkflow def select_distributor # If no order cycles are available this is much faster visit "/" - click_link distributor.name + follow_active_table_node distributor.name end # These methods are naughty and write to the DB directly @@ -18,6 +18,6 @@ module ShopWorkflow end def toggle_accordion(name) - find("dd a", text: name.upcase).click + find("dd a", text: name).click end end diff --git a/spec/support/request/ui_component_helper.rb b/spec/support/request/ui_component_helper.rb new file mode 100644 index 0000000000..df322ec833 --- /dev/null +++ b/spec/support/request/ui_component_helper.rb @@ -0,0 +1,14 @@ +module UIComponentHelper + def open_active_table_row + find("hub:first-child .active_table_row:first-child").click() + end + + def expand_active_table_node(name) + find(".active_table_node", text: name).click + end + + def follow_active_table_node(name) + expand_active_table_node(name) + find(".active_table_node a", text: "Shop at #{name}").click + end +end diff --git a/vendor/assets/javascripts/angular-backstretch.js b/vendor/assets/javascripts/angular-backstretch.js new file mode 100644 index 0000000000..80e14aa4fb --- /dev/null +++ b/vendor/assets/javascripts/angular-backstretch.js @@ -0,0 +1,10 @@ +angular.module('backstretch', []); +angular.module('backstretch') + .directive('backstretch', function () { + return { + restrict: 'A', + link: function (scope, element, attr) { + element.backstretch(attr.backgroundUrl); + } + } + }); diff --git a/vendor/assets/javascripts/angular-flash.min.js b/vendor/assets/javascripts/angular-flash.min.js new file mode 100644 index 0000000000..5e46b88c87 --- /dev/null +++ b/vendor/assets/javascripts/angular-flash.min.js @@ -0,0 +1,6 @@ +/**! + * @license angular-flash v0.1.13 + * Copyright (c) 2013 William L. Bunselmeyer. https://github.com/wmluke/angular-flash + * License: MIT + */ +!function(){"use strict";var a=0,b=function(c){function d(a,b){angular.forEach(j.subscribers,function(c){var d=!c.type||c.type===a,e=!j.id&&!c.id||c.id===j.id;d&&e&&c.cb(b,a)})}var e,f,g,h,i,j=angular.extend({id:null,subscribers:{},classnames:{error:[],warn:[],info:[],success:[]}},c),k=this;this.clean=function(){e=null,f=null,g=null,h=null,i=null},this.subscribe=function(b,c,d){return a+=1,j.subscribers[a]={cb:b,type:c,id:d},a},this.unsubscribe=function(a){delete j.subscribers[a]},this.to=function(a){var c=angular.copy(j);return c.id=a,new b(c)},Object.defineProperty(this,"success",{get:function(){return e},set:function(a){e=a,i="success",d(i,a)}}),Object.defineProperty(this,"info",{get:function(){return f},set:function(a){f=a,i="info",d(i,a)}}),Object.defineProperty(this,"warn",{get:function(){return g},set:function(a){g=a,i="warn",d(i,a)}}),Object.defineProperty(this,"error",{get:function(){return h},set:function(a){h=a,i="error",d(i,a)}}),Object.defineProperty(this,"type",{get:function(){return i}}),Object.defineProperty(this,"message",{get:function(){return i?k[i]:null}}),Object.defineProperty(this,"classnames",{get:function(){return j.classnames}}),Object.defineProperty(this,"id",{get:function(){return j.id}})};angular.module("angular-flash.service",[]).provider("flash",function(){var a=this;this.errorClassnames=["alert-error"],this.warnClassnames=["alert-warn"],this.infoClassnames=["alert-info"],this.successClassnames=["alert-success"],this.$get=function(){return new b({classnames:{error:a.errorClassnames,warn:a.warnClassnames,info:a.infoClassnames,success:a.successClassnames}})}})}(),function(){"use strict";function a(a){return(null===a||void 0===a)&&(a=""),/^\s*$/.test(a)}function b(b,c){return{scope:!0,link:function(d,e,f){function g(){var a=[].concat(b.classnames.error,b.classnames.warn,b.classnames.info,b.classnames.success);angular.forEach(a,function(a){e.removeClass(a)})}function h(h,j){if(i&&c.cancel(i),d.flash.type=j,d.flash.message=h,g(),angular.forEach(b.classnames[j],function(a){e.addClass(a)}),a(f.activeClass)||e.addClass(f.activeClass),!h)return void d.hide();var k=Number(f.duration||5e3);k>0&&(i=c(d.hide,k))}var i,j;d.flash={},d.hide=function(){g(),a(f.activeClass)||e.removeClass(f.activeClass)},d.$on("$destroy",function(){b.clean(),b.unsubscribe(j)}),j=b.subscribe(h,f.flashAlert,f.id),f.flashAlert&&b[f.flashAlert]&&h(b[f.flashAlert],f.flashAlert),!f.flashAlert&&b.message&&h(b.message,b.type)}}}angular.module("angular-flash.flash-alert-directive",["angular-flash.service"]).directive("flashAlert",["flash","$timeout",b])}(); \ No newline at end of file