diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 6e569ed67b..4cce0bdcea 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -9,8 +9,8 @@ //= require jquery_ujs //= require jquery-ui //= require shared/jquery-ui-timepicker-addon -//= require shared/angular -//= require shared/angular-resource +//= require angular +//= require angular-resource //= require admin/spree_core //= require admin/spree_auth //= require admin/spree_promo diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 7a49825291..cb4c1c537b 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -350,4 +350,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/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..65fa9107a9 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -7,11 +7,17 @@ #= 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 angular-flash.min.js # #= require ../shared/jquery.timeago #= require foundation #= require ./darkswarm +#= require ./overrides +#= require_tree ./mixins +#= require_tree ./directives #= require_tree . $ -> 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..2ce5419400 100644 --- a/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/account_sidebar_controller.js.coffee @@ -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/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..7bf69de294 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -1,36 +1,29 @@ -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) -> - $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 + $scope.CheckoutFormState = CheckoutFormState + #$scope.order = Order.order + $scope.accordion = {user: true} - $scope.shippingMethod = -> - $scope.order.shipping_methods[$scope.order.shipping_method_id] + $scope.show = (name)-> + $scope.accordion[name] = true - $scope.shippingMethodChanged = -> - $scope.require_ship_address = $scope.shippingMethod().require_ship_address if $scope.shippingMethod() + storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}"} + console.log "===============================" + console.log "Order.order.id" + + #storage.bind $scope, "accordion.billing", {storeName: "billing_#{Order.order.id}"} + #storage.bind $scope, "accordion.shipping", {storeName: "shipping_#{Order.order.id}"} + #storage.bind $scope, "accordion.payment", {storeName: "payment_#{Order.order.id}"} + + 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/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/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index d2ce7b6758..71d99be03b 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,4 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage']).config ($httpProvider, $tooltipProvider) -> +window.Darkswarm = angular.module("Darkswarm", ["ngResource", "filters", 'mm.foundation', 'angularLocalStorage', 'pasvaz.bindonce', 'infinite-scroll', 'angular-flash.service']).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/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/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..40c1dd6d61 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/checkout_form_state.js.coffee @@ -0,0 +1,4 @@ +Darkswarm.factory 'CheckoutFormState', ()-> + # This class only exists to encapsulate a single field: checkout_state_same_as_billing + # So we can cleanly access it from the Order service as well as the scope + new class CheckoutFormState 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..c3bebdf143 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/order.js.coffee @@ -0,0 +1,53 @@ +Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState, flash)-> + 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]) + + navigate: (path)-> + window.location.pathname = path + + submit: -> + $http.put('/shop/checkout', {order: @preprocess()}).success (data, status)=> + @navigate(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/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/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/forms.css.sass b/app/assets/stylesheets/darkswarm/forms.css.sass index bada1fb68d..e519b2818f 100644 --- a/app/assets/stylesheets/darkswarm/forms.css.sass +++ b/app/assets/stylesheets/darkswarm/forms.css.sass @@ -1,6 +1,6 @@ @import variables -form +.darkswarm fieldset padding: 0px border: none @@ -12,5 +12,23 @@ form display: block width: 100% margin-bottom: 1em + color: #999999 + text-transform: uppercase + dd a + border: 1px solid $dark-grey + border-left: 0px + border-right: 0px + padding: 16px 24px + display: block + width: 100% + margin-bottom: 1em + color: #999999 text-transform: uppercase color: #999999 + font-weight: bold + text-transform: uppercase + background: transparent + .text-right + font-weight: normal + text-transform: none + diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index 1bb79fb905..3f232ad121 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -4,7 +4,7 @@ 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 @@ -12,4 +12,4 @@ module Api respond_with(@enterprises) end end -end \ No newline at end of file +end diff --git a/app/controllers/api/order_cycles_controller.rb b/app/controllers/api/order_cycles_controller.rb index 0796be0b81..a16692832f 100644 --- a/app/controllers/api/order_cycles_controller.rb +++ b/app/controllers/api/order_cycles_controller.rb @@ -3,7 +3,7 @@ module Api respond_to :json def managed @order_cycles = OrderCycle.ransack(params[:q]).result.managed_by(current_api_user) - render :bulk_index + render params[:template] || :bulk_index end def accessible 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/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/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/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 51aaca837d..400d4a33c3 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -15,6 +15,7 @@ %body.off-canvas{"ng-app" => "Darkswarm"} = render partial: "shared/menu" = display_flash_messages + %ofn-flash = render "shared/sidebar" 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..7c5ff51df3 100644 --- a/app/views/shop/shop/_order_cycles.html.haml +++ b/app/views/shop/shop/_order_cycles.html.haml @@ -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..833b338cfa 100644 --- a/app/views/shop/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -1,9 +1,13 @@ -%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.button.right{type: :submit, value: "Add to Cart"} + {{ limit }} + %table %thead %th.name Item @@ -16,17 +20,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", @@ -45,6 +52,7 @@ 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, @@ -52,13 +60,17 @@ 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..fa9b00d5f3 100644 --- a/app/views/shop/shop/_variant.html.haml +++ b/app/views/shop/shop/_variant.html.haml @@ -1,7 +1,7 @@ %td %td.notes -%td {{variant.options_text}} +%td{bindonce: ""} {{variant.options_text}} %td %input{type: :number, value: nil, @@ -16,5 +16,5 @@ 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/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/lib/tasks/karma.rake b/lib/tasks/karma.rake new file mode 100644 index 0000000000..6201c66e61 --- /dev/null +++ b/lib/tasks/karma.rake @@ -0,0 +1,31 @@ +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 + system "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/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb index ed91818fbd..9086fe0953 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/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 9b5ac1dabe..1a227bc6bd 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -507,7 +507,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 @@ -542,7 +542,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..9403c8cac9 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -237,7 +237,7 @@ feature %q{ visit '/admin/products/bulk_edit' - click_link 'New Product' + find("a", text: "New Product").click page.should have_content 'NEW PRODUCT' @@ -783,7 +783,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 +795,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 +869,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/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 7cf28675e1..c1fe87a572 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -35,48 +35,27 @@ feature "As a consumer I want to check out my cart", js: true 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 - 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 + context "on the checkout page" do before do - choose(sm1.name) + visit "/shop/checkout" 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 + 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" 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 - - # Check it keeps state - 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" + context "When shipping method requires an address" do + before do + toggle_accordion "Shipping" + choose(sm1.name) + end + it "shows ship address forms when 'same as billing address' is unchecked" do + uncheck "Shipping address same as billing address?" + find("#ship_address > div.visible").visible?.should be_true + end end end @@ -89,6 +68,7 @@ feature "As a consumer I want to check out my cart", js: true do pm1 # Lazy evaluation of ze create()s pm2 visit "/shop/checkout" + toggle_accordion "Payment Details" end it "shows all available payment methods" do @@ -97,49 +77,55 @@ feature "As a consumer I want to check out my cart", js: true do 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" - 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 + 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 "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" + sleep 10 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 + 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 + 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" - 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 + 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 diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb new file mode 100644 index 0000000000..a01d2e48d4 --- /dev/null +++ b/spec/helpers/checkout_helper_spec.rb @@ -0,0 +1,13 @@ +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", required: true, type: :email} + ) + + 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..615e29535e --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,6 @@ +//= require angular +//= require angular-resource +//= require angular-animate +//= require angular-mocks +//= require angular-cookies +//= require angular-flash.min.js diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index b8930b323e..73dc80c13c 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -23,20 +23,19 @@ describe "AdminOrderMgmtCtrl", -> 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 -> @@ -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/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..f5d5ac6a58 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,27 @@ 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 - + 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/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..f78cec582b --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee @@ -0,0 +1,86 @@ +describe 'Order service', -> + Order = null + orderData = null + $httpBackend = null + CheckoutFormState = 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") + flash = $injector.get("flash") + CheckoutFormState = $injector.get("CheckoutFormState") + spyOn(Order, "navigate") # 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/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/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