diff --git a/Gemfile b/Gemfile
index af4108c9eb..87566eebfa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,6 +35,7 @@ gem 'geocoder'
gem 'gmaps4rails'
gem 'spinjs-rails'
gem 'rack-ssl', :require => 'rack/ssl'
+gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg'
gem 'foreigner'
gem 'immigrant'
diff --git a/Gemfile.lock b/Gemfile.lock
index 87a0acf967..733ef43572 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -6,6 +6,12 @@ GIT
actionpack (~> 3.0)
activemodel (~> 3.0)
+GIT
+ remote: git://github.com/jeremydurham/custom-err-msg.git
+ revision: 3a8ec9dddc7a5b0aab7c69a6060596de300c68f4
+ specs:
+ custom_error_message (1.1.1)
+
GIT
remote: git://github.com/openfoodfoundation/spree.git
revision: da651b40f5c6cdd32e00b060729eb9aefd4f615f
@@ -492,6 +498,7 @@ DEPENDENCIES
coffee-rails (~> 3.2.1)
comfortable_mexican_sofa
compass-rails
+ custom_error_message!
database_cleaner (= 0.7.1)
db2fog
debugger-linecache
diff --git a/app/assets/images/groups.svg b/app/assets/images/groups.svg
new file mode 100644
index 0000000000..f4ca32ec27
--- /dev/null
+++ b/app/assets/images/groups.svg
@@ -0,0 +1,1565 @@
+
+
+
+
diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin.js.coffee
index cf88326ba5..edebc019e5 100644
--- a/app/assets/javascripts/admin/admin.js.coffee
+++ b/app/assets/javascripts/admin/admin.js.coffee
@@ -1,3 +1,3 @@
-angular.module("ofn.admin", ["ngResource","ofn.dropdown"]).config ($httpProvider) ->
+angular.module("ofn.admin", ["ngResource", "ngAnimate", "ofn.dropdown"]).config ($httpProvider) ->
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content")
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js
index 30acc576bf..3fdd45585a 100644
--- a/app/assets/javascripts/admin/all.js
+++ b/app/assets/javascripts/admin/all.js
@@ -11,6 +11,7 @@
//= require shared/jquery-ui-timepicker-addon
//= require angular
//= require angular-resource
+//= require angular-animate
//= require admin/spree_core
//= require admin/spree_auth
//= require admin/spree_promo
diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee
new file mode 100644
index 0000000000..665753a522
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee
@@ -0,0 +1,10 @@
+angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($scope, EnterpriseRelationships, Enterprises) ->
+ $scope.EnterpriseRelationships = EnterpriseRelationships
+ $scope.Enterprises = Enterprises
+
+ $scope.create = ->
+ $scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id)
+
+ $scope.delete = (enterprise_relationship) ->
+ if confirm("Are you sure?")
+ $scope.EnterpriseRelationships.delete enterprise_relationship
diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee
new file mode 100644
index 0000000000..e07b992112
--- /dev/null
+++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee
@@ -0,0 +1,18 @@
+angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterprise_relationships) ->
+ new class EnterpriseRelationships
+ create_errors: ""
+
+ constructor: ->
+ @enterprise_relationships = enterprise_relationships
+
+ create: (parent_id, child_id) ->
+ $http.post('/admin/enterprise_relationships', {enterprise_relationship: {parent_id: parent_id, child_id: child_id}}).success (data, status) =>
+ @enterprise_relationships.unshift(data)
+ @create_errors = ""
+
+ .error (response, status) =>
+ @create_errors = response.errors
+
+ delete: (er) ->
+ $http.delete('/admin/enterprise_relationships/' + er.id).success (data) =>
+ @enterprise_relationships.splice @enterprise_relationships.indexOf(er), 1
diff --git a/app/assets/javascripts/admin/services/enterprises.js.coffee b/app/assets/javascripts/admin/services/enterprises.js.coffee
new file mode 100644
index 0000000000..5b5fd6a009
--- /dev/null
+++ b/app/assets/javascripts/admin/services/enterprises.js.coffee
@@ -0,0 +1,5 @@
+angular.module("ofn.admin").factory 'Enterprises', (my_enterprises, all_enterprises) ->
+ new class Enterprises
+ constructor: ->
+ @my_enterprises = my_enterprises
+ @all_enterprises = all_enterprises
diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee
index 20d849f60c..2c07d069c4 100644
--- a/app/assets/javascripts/darkswarm/all.js.coffee
+++ b/app/assets/javascripts/darkswarm/all.js.coffee
@@ -5,6 +5,7 @@
#
#= require angular
#= require angular-cookies
+#= require angular-sanitize
#= require angular-resource
#= require ../shared/mm-foundation-tpls-0.2.0-SNAPSHOT
#= require ../shared/bindonce.min.js
diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee
index c2211bc033..da625e3d34 100644
--- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee
+++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee
@@ -1,8 +1,11 @@
-Darkswarm.controller "LoginCtrl", ($scope, $http, $location, AuthenticationService) ->
+Darkswarm.controller "LoginCtrl", ($scope, $http, AuthenticationService, Redirections) ->
$scope.path = "/login"
$scope.submit = ->
$http.post("/user/spree_user/sign_in", {spree_user: $scope.spree_user}).success (data)->
- location.href = location.origin + location.pathname # Strips out hash fragments
+ if Redirections.after_login
+ location.href = location.origin + Redirections.after_login
+ else
+ location.href = location.origin + location.pathname # Strips out hash fragments
.error (data) ->
$scope.errors = data.message
diff --git a/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee
new file mode 100644
index 0000000000..850f58b33e
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee
@@ -0,0 +1,6 @@
+Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) ->
+ $scope.Groups = Groups
+ $scope.order = 'position'
+
+ $rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) ->
+ $anchorScroll()
diff --git a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee
index 29e81ae861..555e49c539 100644
--- a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee
+++ b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee
@@ -1,15 +1,17 @@
-Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle, $timeout) ->
+Darkswarm.controller "OrderCycleCtrl", ($scope, OrderCycle, $timeout) ->
$scope.order_cycle = OrderCycle.order_cycle
$scope.OrderCycle = OrderCycle
- $scope.changeOrderCycle = ->
- OrderCycle.push_order_cycle()
- $timeout ->
- $("#order_cycle_id").trigger("closeTrigger")
-
# Timeout forces this to be evaluated after everything is loaded
# This is a hack. We should probably write our own "popover" directive
# That takes an expression instead of a trigger, and binds to that
$timeout =>
if !$scope.OrderCycle.selected()
$("#order_cycle_id").trigger("openTrigger")
+
+
+Darkswarm.controller "OrderCycleChangeCtrl", ($scope, OrderCycle, Product, $timeout) ->
+ $scope.changeOrderCycle = ->
+ OrderCycle.push_order_cycle Product.update
+ $timeout ->
+ $("#order_cycle_id").trigger("closeTrigger")
diff --git a/app/assets/javascripts/darkswarm/controllers/producer_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/producer_node_controller.js.coffee
index 2f3bf1ce00..d46106a830 100644
--- a/app/assets/javascripts/darkswarm/controllers/producer_node_controller.js.coffee
+++ b/app/assets/javascripts/darkswarm/controllers/producer_node_controller.js.coffee
@@ -1,6 +1,6 @@
Darkswarm.controller "ProducerNodeCtrl", ($scope, HashNavigation, $anchorScroll) ->
$scope.toggle = ->
- HashNavigation.navigate $scope.producer.hash
+ HashNavigation.toggle $scope.producer.hash
$scope.open = ->
HashNavigation.active($scope.producer.hash)
diff --git a/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee
new file mode 100644
index 0000000000..f016d2e43f
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee
@@ -0,0 +1,11 @@
+Darkswarm.controller "ProductNodeCtrl", ($scope) ->
+
+ $scope.price = ->
+ if $scope.product.variants.length > 0
+ prices = (v.price for v in $scope.product.variants)
+ Math.min.apply(null, prices)
+ else
+ $scope.product.price
+
+ $scope.producer = $scope.product.supplier
+ $scope.hasVariants = $scope.product.variants.length > 0
diff --git a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee
index 9ba11baa5b..323df2fc4e 100644
--- a/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee
+++ b/app/assets/javascripts/darkswarm/controllers/products_controller.js.coffee
@@ -1,8 +1,8 @@
Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) ->
$scope.data = Product.data
$scope.limit = 3
+ $scope.ordering = {order: "name"}
$scope.order_cycle = OrderCycle.order_cycle
- Product.update()
$scope.incrementLimit = ->
if $scope.limit < $scope.data.products.length
@@ -12,10 +12,3 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Product, OrderCycle) -
code = e.keyCode || e.which
if code == 13
e.preventDefault()
-
- $scope.productPrice = (product) ->
- if product.variants.length > 0
- prices = (v.price for v in product.variants)
- Math.min.apply(null, prices)
- else
- product.price
diff --git a/app/assets/javascripts/darkswarm/controllers/tabs/producers_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/tabs/producers_controller.js.coffee
new file mode 100644
index 0000000000..92cf8dedfc
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/controllers/tabs/producers_controller.js.coffee
@@ -0,0 +1,2 @@
+Darkswarm.controller "ProducersTabCtrl", ($scope, CurrentHub) ->
+ $scope.CurrentHub = CurrentHub
diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee
index fe128b963a..8d26384c14 100644
--- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee
+++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee
@@ -5,6 +5,7 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource",
'infinite-scroll',
'angular-flash.service',
'templates',
+ 'ngSanitize',
'backstretch']).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) ->
$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')
diff --git a/app/assets/javascripts/darkswarm/directives/modal.js.coffee b/app/assets/javascripts/darkswarm/directives/modal.js.coffee
index 8c34ec18a8..c8ed5df4af 100644
--- a/app/assets/javascripts/darkswarm/directives/modal.js.coffee
+++ b/app/assets/javascripts/darkswarm/directives/modal.js.coffee
@@ -2,13 +2,16 @@ Darkswarm.directive "ofnModal", ($modal)->
restrict: 'E'
replace: true
transclude: true
+ scope: {}
template: "{{title}}"
link: (scope, elem, attrs, ctrl, transclude)->
scope.title = attrs.title
+ contents = null
+ # We're using an isolate scope, which is a child of the original scope
+ # We have to compile the transclude against the original scope, not the isolate
+ transclude scope.$parent, (clone)->
+ contents = clone
- scope.cancel = ->
- scope.modalInstance.dismiss("cancel")
-
- elem.on "click", ->
- scope.modalInstance = $modal.open(controller: ctrl, template: transclude())
+ elem.on "click", =>
+ scope.modalInstance = $modal.open(controller: ctrl, template: contents, scope: scope.$parent)
diff --git a/app/assets/javascripts/darkswarm/filters/strip_url.js.coffee b/app/assets/javascripts/darkswarm/filters/strip_url.js.coffee
new file mode 100644
index 0000000000..dcc86b600a
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/filters/strip_url.js.coffee
@@ -0,0 +1,6 @@
+Darkswarm.filter "stripUrl", ->
+ stripper = /(https?:\/\/)?(www\.)?(.*)/
+ (url) ->
+ url.match(stripper).pop()
+
+
diff --git a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee
index eada8bc44b..820d5bde9c 100644
--- a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee
@@ -1,4 +1,4 @@
-Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location)->
+Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redirections)->
new class AuthenticationService
selectedPath: "/login"
diff --git a/app/assets/javascripts/darkswarm/services/groups.js.coffee b/app/assets/javascripts/darkswarm/services/groups.js.coffee
new file mode 100644
index 0000000000..e5e50615e8
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/services/groups.js.coffee
@@ -0,0 +1,4 @@
+Darkswarm.factory 'Groups', (groups) ->
+ new class Groups
+ constructor: ->
+ @groups = groups
diff --git a/app/assets/javascripts/darkswarm/services/order.js.coffee b/app/assets/javascripts/darkswarm/services/order.js.coffee
index 2357fd42da..8078cf8fc3 100644
--- a/app/assets/javascripts/darkswarm/services/order.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/order.js.coffee
@@ -1,4 +1,4 @@
-Darkswarm.factory 'Order', ($resource, Product, order, $http, CheckoutFormState, flash, Navigation)->
+Darkswarm.factory 'Order', ($resource, order, $http, CheckoutFormState, flash, Navigation)->
new class Order
errors: {}
diff --git a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee
index 1bb23446a2..0c5176328b 100644
--- a/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/order_cycle.js.coffee
@@ -1,10 +1,10 @@
-Darkswarm.factory 'OrderCycle', ($resource, Product, orderCycleData) ->
+Darkswarm.factory 'OrderCycle', ($resource, orderCycleData) ->
class OrderCycle
@order_cycle = orderCycleData # Object or {} due to RABL
- @push_order_cycle: ->
+ @push_order_cycle: (callback) ->
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()
+ callback()
@orders_close_at: ->
@order_cycle.orders_close_at if @selected()
diff --git a/app/assets/javascripts/darkswarm/services/product.js.coffee b/app/assets/javascripts/darkswarm/services/product.js.coffee
index 89778b659c..406e122de1 100644
--- a/app/assets/javascripts/darkswarm/services/product.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/product.js.coffee
@@ -1,12 +1,15 @@
Darkswarm.factory 'Product', ($resource) ->
new class Product
- data: {
+ constructor: ->
+ @update()
+
+ # TODO: don't need to scope this into object
+ # Already on object as far as controller scope is concerned
+ data:
products: null
loading: true
- }
- update: ->
+
+ update: =>
@data.products = $resource("/shop/products").query =>
@data.loading = false
@data
- all: ->
- @data.products || @update()
diff --git a/app/assets/javascripts/darkswarm/services/redirections.js.coffee b/app/assets/javascripts/darkswarm/services/redirections.js.coffee
new file mode 100644
index 0000000000..a479964e9e
--- /dev/null
+++ b/app/assets/javascripts/darkswarm/services/redirections.js.coffee
@@ -0,0 +1,3 @@
+Darkswarm.factory "Redirections", ($location)->
+ new class Redirections
+ after_login: $location.search().after_login
diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js
index d4fb9527d8..972b74247f 100644
--- a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js
+++ b/app/assets/javascripts/shared/mm-foundation-tpls-0.2.0-SNAPSHOT.js
@@ -2477,7 +2477,7 @@ angular.module("template/modal/window.html", []).run(["$templateCache", function
$templateCache.put("template/modal/window.html",
"
\n" +
+ " style=\"display: block; visibility: visible\">\n" +
"
\n" +
"
\n" +
"");
diff --git a/app/assets/stylesheets/admin/enterprise_relationships.css.sass b/app/assets/stylesheets/admin/enterprise_relationships.css.sass
new file mode 100644
index 0000000000..cffc0b0623
--- /dev/null
+++ b/app/assets/stylesheets/admin/enterprise_relationships.css.sass
@@ -0,0 +1,20 @@
+// TODO: Provide -moz- and -o- directives
+@-webkit-keyframes alert-flash
+ 0%
+ background-color: #f9f1ae
+
+ 100%
+ background-color: #fff
+
+
+table#enterprise-relationships
+ th.actions, td.actions
+ width: 16%
+ .errors
+ color: #f00
+
+ tr.ng-enter
+ -webkit-animation-name: alert-flash
+ -webkit-animation-duration: 1200ms
+ -webkit-animation-iteration-count: 1
+ -webkit-animation-timing-function: ease-in-out
diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss
index 92092bd136..87983a6bc7 100644
--- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss
+++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss
@@ -19,9 +19,11 @@ table .blank-action {
}
+input.search {
+ margin-bottom: 1em;
+}
#new_enterprise_fee_set input.search {
float: right;
- margin-bottom: 1em;
}
.ng .ng-invalid.ng-dirty {
diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss
index a3ee62ec56..80e43cdc8b 100644
--- a/app/assets/stylesheets/darkswarm/all.scss
+++ b/app/assets/stylesheets/darkswarm/all.scss
@@ -11,5 +11,4 @@
ofn-modal {
display: block;
-}
-
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/darkswarm/forms.css.sass b/app/assets/stylesheets/darkswarm/forms.css.sass
new file mode 100644
index 0000000000..7047159601
--- /dev/null
+++ b/app/assets/stylesheets/darkswarm/forms.css.sass
@@ -0,0 +1,5 @@
+@import mixins
+@import branding
+
+fieldset
+ border: 0
\ No newline at end of file
diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass
new file mode 100644
index 0000000000..b859273ab7
--- /dev/null
+++ b/app/assets/stylesheets/darkswarm/groups.css.sass
@@ -0,0 +1,42 @@
+@import branding
+@import mixins
+
+#groups
+ background-color: $clr-brick-light
+ background-image: url("/assets/groups.svg")
+ background-position: center 15px
+ background-repeat: no-repeat
+ padding-bottom: 20px
+
+.group
+ padding-bottom: 40px
+ hr
+ border-bottom: 10px solid white
+ outline: 0
+ border-top: 0
+ margin: 0
+
+.group-hero
+ position: relative
+ padding: 0
+ border: 10px solid white
+ background: white
+
+h3.group-name
+ margin-top: 0.5em
+ margin-bottom: 0.15em
+
+img.group-logo
+ max-width: 220px
+ max-height: 86px
+ float: right
+ padding-top: 10px
+
+
+img.group-hero-img
+ background-color: black
+ width: 100%
+ height: inherit
+ max-height: 260px
+ min-height: 120px
+ overflow: hidden
\ No newline at end of file
diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass
index 1f8086d9f2..fd26cfcae9 100644
--- a/app/assets/stylesheets/darkswarm/mixins.sass
+++ b/app/assets/stylesheets/darkswarm/mixins.sass
@@ -12,7 +12,7 @@
border: 1px solid #999
font-size: 18px
@extend .avenir
- padding: 22px 18px
+ padding: 0.75em 1em
height: auto
margin-bottom: 1em
@@ -85,10 +85,10 @@
&, & *
color: $clr-turquoise
a
- color: white
+ color: $clr-turquoise
&:hover
text-decoration: none
- color: $clr-turquoise-light
+ color: $clr-turquoise-bright
@mixin fullbg
background-position: center center
diff --git a/app/assets/stylesheets/darkswarm/product_table.css.sass b/app/assets/stylesheets/darkswarm/product_table.css.sass
new file mode 100644
index 0000000000..9b0a7b4598
--- /dev/null
+++ b/app/assets/stylesheets/darkswarm/product_table.css.sass
@@ -0,0 +1,4 @@
+.product_table
+ .row
+ border: 1px solid black
+ padding: 8px inherit
diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass
index 2107542594..0c2972367a 100644
--- a/app/assets/stylesheets/darkswarm/shop.css.sass
+++ b/app/assets/stylesheets/darkswarm/shop.css.sass
@@ -1,8 +1,7 @@
@import mixins
@import variables
+@import branding
-product
- display: block
.darkswarm
#search
@@ -77,68 +76,58 @@ product
products
display: block
- padding-top: 2.3em
+ padding-top: 2.3em
@media all and (max-width: 768px)
padding-top: 1em
input.button.right
float: left
- table
- table-layout: fixed
- width: 100%
- border-collapse: collapse
- border: none
- th
- line-height: 50px
- &.name
- width: 330px
- //&.notes
- //width: 140px
- &.variant
- width: 180px
- &.quantity, &.bulk, &.price
- width: 90px
- .notes
- max-width: 300px
- td, th
+
+
+ product:hover, product:focus, product:active
+ border-color: $clr-brick
+ @include box-shadow(0 0 3px 0 $clr-brick-bright)
+
+ .row.variants
+ border-top: 1px solid $clr-brick-light
+ background: $clr-brick-ultra-light
+
+ product
+ @include csstrans
+ border: 1px solid #989898
+ display: block
+ margin-bottom: 1em !important
+
+ input
+ margin: 0
+ width: 8em
+
+ .columns
+ padding-top: 1em
+ padding-bottom: 1em
+
+ .row.summary, .row.variants
+ @include csstrans
+ margin-left: 0
+ margin-right: 0
+ background: #f7f7f7
+ border-top: 1px solid #dfdfdf
+
+ .row.summary
+ @include csstrans
background: #fff
- > span
- min-width: 50px
- display: block
- tbody
- border: 1px solid #cccccc
- border-left: 0px
- border-right: 0px
- td
- padding: 20px 0px
- &.name
- img
- float: left
- margin-right: 30px
- @media all and (max-width: 768px)
- margin-right: 1em
- div
- min-width: 150px
- tr.product-description
- display: none
+
+ .summary-header
+ &, & *
+ @include avenir
+ color: $clr-brick
+
+ .summary-price
+ &, & *
+ @include avenir
+
+
+
- // Responsive
- @media all and (max-width: 768px)
- td.notes, th.notes
- display: none
- img
- width: 20px
- height: auto
- tr.product-description
- display: table-row
- td:empty
- display: none
-
- input[type=number]
- width: 60px
- margin: 0px
- display: block
- float: right
- padding-top: 14px
diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass
new file mode 100644
index 0000000000..a5314c3ef2
--- /dev/null
+++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass
@@ -0,0 +1,6 @@
+@import mixins
+@import branding
+
+#edit-cart
+ button, .button
+ margin: 0
diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass
index 68d990240d..63d4efe13f 100644
--- a/app/assets/stylesheets/darkswarm/typography.css.sass
+++ b/app/assets/stylesheets/darkswarm/typography.css.sass
@@ -26,7 +26,7 @@ a
text-decoration: none
color: $clr-brick-bright
-small
+small, .small
font-size: 0.75rem
@mixin avenir
@@ -56,6 +56,9 @@ ul.ofn-list
.pad-top
padding-top: 1em
+.not-bold
+ font-weight: normal
+
strong.avenir
font-weight: normal // Avenir is basically bold anyway
diff --git a/app/assets/stylesheets/groups.css.scss b/app/assets/stylesheets/groups.css.scss
new file mode 100644
index 0000000000..c2a5f9013b
--- /dev/null
+++ b/app/assets/stylesheets/groups.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the groups controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb
index 466bde2cd5..cc2f3ed1db 100644
--- a/app/controllers/admin/enterprise_groups_controller.rb
+++ b/app/controllers/admin/enterprise_groups_controller.rb
@@ -15,12 +15,10 @@ module Admin
redirect_to main_app.admin_enterprise_groups_path
end
-
private
def collection
EnterpriseGroup.by_position
end
-
end
end
diff --git a/app/controllers/admin/enterprise_relationships_controller.rb b/app/controllers/admin/enterprise_relationships_controller.rb
new file mode 100644
index 0000000000..212bf3849d
--- /dev/null
+++ b/app/controllers/admin/enterprise_relationships_controller.rb
@@ -0,0 +1,25 @@
+module Admin
+ class EnterpriseRelationshipsController < ResourceController
+ def index
+ @my_enterprises = Enterprise.managed_by(spree_current_user).by_name
+ @all_enterprises = Enterprise.by_name
+ @enterprise_relationships = EnterpriseRelationship.by_name.involving_enterprises @my_enterprises
+ end
+
+ def create
+ @enterprise_relationship = EnterpriseRelationship.new params[:enterprise_relationship]
+
+ if @enterprise_relationship.save
+ render partial: "admin/json/enterprise_relationship", locals: {enterprise_relationship: @enterprise_relationship}
+ else
+ render status: 400, json: {errors: @enterprise_relationship.errors.full_messages.join(', ')}
+ end
+ end
+
+ def destroy
+ @enterprise_relationship = EnterpriseRelationship.find params[:id]
+ @enterprise_relationship.destroy
+ render nothing: true
+ end
+ end
+end
diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb
index 46273d0eff..566c660112 100644
--- a/app/controllers/admin/order_cycles_controller.rb
+++ b/app/controllers/admin/order_cycles_controller.rb
@@ -23,7 +23,7 @@ module Admin
respond_to do |format|
if @order_cycle.save
- OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle).go!
+ OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go!
flash[:notice] = 'Your order cycle has been created.'
format.html { redirect_to admin_order_cycles_path }
@@ -40,7 +40,7 @@ module Admin
respond_to do |format|
if @order_cycle.update_attributes(params[:order_cycle])
- OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle).go!
+ OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, managed_enterprises).go!
flash[:notice] = 'Your order cycle has been updated.'
format.html { redirect_to admin_order_cycles_path }
diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb
index 5cf940c223..3879f16741 100644
--- a/app/controllers/checkout_controller.rb
+++ b/app/controllers/checkout_controller.rb
@@ -26,7 +26,11 @@ class CheckoutController < Spree::CheckoutController
if @order.next
state_callback(:after)
else
- flash[:error] = t(:payment_processing_failed)
+ unless @order.errors.empty?
+ flash[:error] = @order.errors.full_messages.to_sentence
+ else
+ flash[:error] = t(:payment_processing_failed)
+ end
update_failed
return
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
new file mode 100644
index 0000000000..84290f20a3
--- /dev/null
+++ b/app/controllers/groups_controller.rb
@@ -0,0 +1,7 @@
+class GroupsController < BaseController
+ layout 'darkswarm'
+
+ def index
+ @groups = EnterpriseGroup.on_front_page.by_position
+ end
+end
diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb
index 8e876513fa..df7076345f 100644
--- a/app/controllers/spree/admin/base_controller_decorator.rb
+++ b/app/controllers/spree/admin/base_controller_decorator.rb
@@ -11,4 +11,4 @@ Spree::Admin::BaseController.class_eval do
authorize! :admin, record
authorize! action, record
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb
index dd53294f6f..a6142d26c0 100644
--- a/app/controllers/spree/admin/overview_controller_decorator.rb
+++ b/app/controllers/spree/admin/overview_controller_decorator.rb
@@ -4,4 +4,16 @@ Spree::Admin::OverviewController.class_eval do
@product_count = Spree::Product.active.managed_by(spree_current_user).count
@order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count
end
-end
\ No newline at end of file
+
+ # This is in Spree::Core::ControllerHelpers::Auth
+ # But you can't easily reopen modules in Ruby
+ def unauthorized
+ if try_spree_current_user
+ flash[:error] = t(:authorization_failure)
+ redirect_to '/unauthorized'
+ else
+ store_location
+ redirect_to root_path(anchor: "login?after_login=#{spree.admin_path}")
+ end
+ end
+end
diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb
index af1d5621c5..959e51204f 100644
--- a/app/controllers/spree/orders_controller_decorator.rb
+++ b/app/controllers/spree/orders_controller_decorator.rb
@@ -11,6 +11,16 @@ Spree::OrdersController.class_eval do
include OrderCyclesHelper
layout 'darkswarm'
+ # Patching to redirect to shop if order is empty
+ def edit
+ @order = current_order(true)
+ if @order.line_items.empty?
+ redirect_to main_app.shop_path
+ else
+ associate_user
+ end
+ end
+
# Patch Orders#populate to populate multi_cart (if enabled)
def populate
if OpenFoodNetwork::FeatureToggle.enabled? :multi_cart
@@ -18,6 +28,7 @@ Spree::OrdersController.class_eval do
end
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
if populator.populate(params.slice(:products, :variants, :quantity))
+
fire_event('spree.cart.add')
fire_event('spree.order.contents_changed')
respond_with(@order) do |format|
diff --git a/app/controllers/spree/store_controller_decorator.rb b/app/controllers/spree/store_controller_decorator.rb
new file mode 100644
index 0000000000..09c2efc7b4
--- /dev/null
+++ b/app/controllers/spree/store_controller_decorator.rb
@@ -0,0 +1,7 @@
+class Spree::StoreController
+ layout 'darkswarm'
+
+ def unauthorized
+ render 'shared/unauthorized', :status => 401
+ end
+end
diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb
index 4b0f2f4a93..ed22831589 100644
--- a/app/helpers/enterprises_helper.rb
+++ b/app/helpers/enterprises_helper.rb
@@ -2,6 +2,10 @@ module EnterprisesHelper
def current_distributor
@current_distributor ||= current_order(false).andand.distributor
end
+
+ def managed_enterprises
+ Enterprise.managed_by(spree_current_user)
+ end
def enterprises_options enterprises
enterprises.map { |enterprise| [enterprise.name + ": " + enterprise.address.address1 + ", " + enterprise.address.city, enterprise.id.to_i] }
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
new file mode 100644
index 0000000000..c091b2fc82
--- /dev/null
+++ b/app/helpers/groups_helper.rb
@@ -0,0 +1,2 @@
+module GroupsHelper
+end
diff --git a/app/helpers/shared_helper.rb b/app/helpers/shared_helper.rb
index f255999009..f856a088d1 100644
--- a/app/helpers/shared_helper.rb
+++ b/app/helpers/shared_helper.rb
@@ -1,4 +1,9 @@
module SharedHelper
+
+ def inject_json(name, partial)
+ render "json/injection", name: name, partial: partial
+ end
+
def distributor_link_class(distributor)
cart = current_order(true)
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
@@ -9,17 +14,11 @@ module SharedHelper
klass
end
- # all suppliers of current distributor's products
- def current_producers
- if current_distributor && current_order_cycle
- variants = current_order_cycle.variants_distributed_by(current_distributor)
- Enterprise.supplying_variant_in(variants)
- else
- []
- end
- end
-
def enterprise_user?
- spree_current_user.andand.enterprises.count > 0
+ spree_current_user.andand.enterprises.andand.count.to_i > 0
+ end
+
+ def admin_user?
+ spree_current_user.andand.has_spree_role? 'admin'
end
end
diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb
index 0b700ba596..dd40744492 100644
--- a/app/models/enterprise.rb
+++ b/app/models/enterprise.rb
@@ -18,8 +18,8 @@ class Enterprise < ActiveRecord::Base
delegate :latitude, :longitude, :city, :state_name, :to => :address
accepts_nested_attributes_for :address
- has_attached_file :logo, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png"
- has_attached_file :promo_image, :styles => { :large => "570x380>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png"
+ has_attached_file :logo, :styles => { :medium => "300x300>", :thumb => "100x100>" }
+ has_attached_file :promo_image, :styles => { :large => "260x1200#", :thumb => "100x100>" }
validates_presence_of :name
validates_presence_of :address
diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb
index 41d2a997cc..1cea76db72 100644
--- a/app/models/enterprise_group.rb
+++ b/app/models/enterprise_group.rb
@@ -4,6 +4,17 @@ class EnterpriseGroup < ActiveRecord::Base
has_and_belongs_to_many :enterprises
validates :name, presence: true
+ validates :description, presence: true
+
+ attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids
+
+ attr_accessible :promo_image
+ has_attached_file :promo_image, styles: {large: "260x1200#"}
+ validates_attachment_content_type :promo_image, :content_type => /\Aimage\/.*\Z/
+
+ attr_accessible :logo
+ has_attached_file :logo, styles: {medium: "100x100"}
+ validates_attachment_content_type :logo, :content_type => /\Aimage\/.*\Z/
scope :by_position, order('position ASC')
scope :on_front_page, where(on_front_page: true)
diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb
index 2cbda1511a..26bf8fc012 100644
--- a/app/models/enterprise_relationship.rb
+++ b/app/models/enterprise_relationship.rb
@@ -3,5 +3,14 @@ class EnterpriseRelationship < ActiveRecord::Base
belongs_to :child, class_name: 'Enterprise'
validates_presence_of :parent_id, :child_id
- validates_uniqueness_of :child_id, scope: :parent_id
+ validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established."
+
+ scope :with_enterprises,
+ joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id').
+ joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id')
+ scope :by_name, with_enterprises.order('parent_enterprises.name, child_enterprises.name')
+
+ scope :involving_enterprises, ->(enterprises) {
+ where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises)
+ }
end
diff --git a/app/models/exchange.rb b/app/models/exchange.rb
index 18c3f81447..c491748d5d 100644
--- a/app/models/exchange.rb
+++ b/app/models/exchange.rb
@@ -54,6 +54,10 @@ class Exchange < ActiveRecord::Base
incoming? ? 'supplier' : 'distributor'
end
+ def participant
+ incoming? ? sender : receiver
+ end
+
def to_h(core_only=false)
h = attributes.merge({ 'variant_ids' => variant_ids.sort, 'enterprise_fee_ids' => enterprise_fee_ids.sort })
h.reject! { |k| %w(id order_cycle_id created_at updated_at).include? k } if core_only
diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb
index ee83e85bf7..44e7d5e02e 100644
--- a/app/models/spree/ability_decorator.rb
+++ b/app/models/spree/ability_decorator.rb
@@ -1,6 +1,6 @@
-
class AbilityDecorator
include CanCan::Ability
+
def initialize(user)
if user.enterprises.count > 0
@@ -53,6 +53,11 @@ class AbilityDecorator
(user.enterprises & shipping_method.distributors).any?
end
+ can [:admin, :index, :create], EnterpriseRelationship
+ can [:destroy], EnterpriseRelationship do |enterprise_relationship|
+ user.enterprises.include? enterprise_relationship.parent
+ end
+
can [:create], OrderCycle
can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle|
user.enterprises.include? order_cycle.coordinator
diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb
index 4b93e0427e..972befe54a 100644
--- a/app/models/spree/product_decorator.rb
+++ b/app/models/spree/product_decorator.rb
@@ -124,6 +124,10 @@ Spree::Product.class_eval do
order_cycle.variants_distributed_by(distributor).where(product_id: self)
end
+ def primary_taxon
+ self.taxons.order.first
+ end
+
# Build a product distribution for each distributor
def build_product_distributions_for_user user
Enterprise.is_distributor.managed_by(user).each do |distributor|
diff --git a/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface b/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface
new file mode 100644
index 0000000000..efd7ccbfaf
--- /dev/null
+++ b/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface
@@ -0,0 +1,3 @@
+/ insert_top "[data-hook='admin_product_form_right']"
+
+= render 'spree/admin/products/primary_taxon_form', f: f
\ No newline at end of file
diff --git a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface
index efe611e156..7e4c0f0b3e 100644
--- a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface
+++ b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface
@@ -35,27 +35,24 @@
= f.label :product_variant_unit_name, :unit_name
%input.fullwidth{ id: 'product_variant_unit_name','ng-model' => 'product.variant_unit_name', :name => 'product[variant_unit_name]', :placeholder => 'eg. bunches', :type => 'text' }
.twelve.columns.alpha
- .three.columns.alpha
+ .six.columns.alpha
+ = render 'spree/admin/products/primary_taxon_form', f: f
+ .three.columns
= f.field_container :price do
= f.label :price, t(:price)
%span.required *
%br/
= f.text_field :price, class: 'fullwidth'
= f.error_message_on :price
- .three.columns
+ .three.columns.omega
= f.field_container :on_hand do
= f.label :on_hand, t(:on_hand)
%span.required *
%br/
= f.text_field :on_hand, class: 'fullwidth'
= f.error_message_on :on_hand
- .six.columns.omega
- = f.field_container :primary_taxon do
- = f.label :product_category
- %br/
- = text_field_tag :primary_taxon, nil, class: 'fullwidth'
.twelve.columns.alpha
- = f.field_container :on_hand do
+ = f.field_container :description do
= f.label :product_description, t(:product_description)
%br/
= f.text_area :description, class: 'fullwidth', rows: 3
@@ -82,4 +79,4 @@
:javascript
angular.element(document.getElementById("new_product")).ready(function() {
angular.bootstrap(document.getElementById("new_product"), ['admin.products']);
- });
\ No newline at end of file
+ });
diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml
index b41a68d4a5..ec4934bf19 100644
--- a/app/views/admin/enterprise_groups/_form.html.haml
+++ b/app/views/admin/enterprise_groups/_form.html.haml
@@ -3,6 +3,16 @@
%br/
= f.text_field :name
+= f.field_container :description do
+ = f.label :description
+ %br/
+ = f.text_field :description
+
+= f.field_container :long_description do
+ = f.label :long_description
+ %br/
+ = f.text_area :long_description
+
= f.field_container :on_front_page do
= f.label :on_front_page, 'On front page?'
%br/
@@ -12,3 +22,22 @@
= f.label :enterprise_ids, 'Enterprises'
%br/
= f.collection_select :enterprise_ids, Enterprise.all, :id, :name, {}, {class: "select2 fullwidth", multiple: true}
+
+
+.row
+ .alpha.three.columns
+ = f.label :logo, class: 'with-tip', 'data-powertip' => 'This is the logo'
+ .with-tip{'data-powertip' => 'This is the logo'}
+ %a What's this?
+ .omega.eight.columns
+ = image_tag @object.logo.url if @object.logo.present?
+ = f.file_field :logo
+
+.row
+ .alpha.three.columns
+ = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed at the top of the Group profile'
+ .with-tip{'data-powertip' => 'This image is displayed at the top of the Group profile'}
+ %a What's this?
+ .omega.eight.columns
+ = image_tag @object.promo_image.url if @object.promo_image.present?
+ = f.file_field :promo_image
diff --git a/app/views/admin/enterprise_relationships/_data.html.haml b/app/views/admin/enterprise_relationships/_data.html.haml
new file mode 100644
index 0000000000..7c13978e90
--- /dev/null
+++ b/app/views/admin/enterprise_relationships/_data.html.haml
@@ -0,0 +1,4 @@
+:javascript
+ angular.module('ofn.admin').value('enterprise_relationships', #{render partial: "admin/json/enterprise_relationships", object: @enterprise_relationships});
+ angular.module('ofn.admin').value('my_enterprises', #{render partial: "admin/json/enterprises", object: @my_enterprises});
+ angular.module('ofn.admin').value('all_enterprises', #{render partial: "admin/json/enterprises", object: @all_enterprises});
diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml
new file mode 100644
index 0000000000..3f93027ca1
--- /dev/null
+++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml
@@ -0,0 +1,5 @@
+%td {{ enterprise_relationship.parent_name }}
+%td permits
+%td {{ enterprise_relationship.child_name }}
+%td.actions
+ %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'}
diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml
new file mode 100644
index 0000000000..1a737ec3a5
--- /dev/null
+++ b/app/views/admin/enterprise_relationships/_form.html.haml
@@ -0,0 +1,9 @@
+%tr
+ %td
+ %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"}
+ %td permits
+ %td
+ %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"}
+ %td.actions
+ %input{type: "button", value: "Create", "ng-click" => "create()"}
+ .errors {{ EnterpriseRelationships.create_errors }}
diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml
new file mode 100644
index 0000000000..e0e289efcf
--- /dev/null
+++ b/app/views/admin/enterprise_relationships/index.html.haml
@@ -0,0 +1,15 @@
+- content_for :page_title do
+ Enterprise Relationships
+
+= render 'admin/shared/enterprises_sub_menu'
+
+%div{"ng-app" => "ofn.admin", "ng-controller" => "AdminEnterpriseRelationshipsCtrl"}
+ = render 'data'
+
+ %input.search{"ng-model" => "query", "placeholder" => "Search"}
+
+ %table#enterprise-relationships
+ %tbody
+ = render 'form'
+ %tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | filter:query"}
+ = render 'enterprise_relationship'
diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml
index ab795d21fd..161f43d34d 100644
--- a/app/views/admin/enterprises/_form.html.haml
+++ b/app/views/admin/enterprises/_form.html.haml
@@ -116,12 +116,21 @@
= f.label :website
.omega.eight.columns
= f.text_field :website, { placeholder: "eg. www.truffles.com"}
- -# TODO: Facebook model field
- -#.row
- -# .alpha.two.columns
- -# = f.label :facebook, 'Facebook'
- -# .omega.four.columns
- -# = f.text_field :facebook
+ .row
+ .alpha.two.columns
+ = f.label :facebook, 'Facebook'
+ .omega.four.columns
+ = f.text_field :facebook
+ .row
+ .alpha.two.columns
+ = f.label :instagram, 'Instagram'
+ .omega.four.columns
+ = f.text_field :instagram
+ .row
+ .alpha.two.columns
+ = f.label :linkedin, 'LinkedIn'
+ .omega.four.columns
+ = f.text_field :linkedin
.row
.alpha.three.columns
= f.label :twitter
diff --git a/app/views/admin/enterprises/index.html.erb b/app/views/admin/enterprises/index.html.erb
index d2917bcc45..1795b17950 100644
--- a/app/views/admin/enterprises/index.html.erb
+++ b/app/views/admin/enterprises/index.html.erb
@@ -8,6 +8,8 @@
<% end %>
+<%= render 'admin/shared/enterprises_sub_menu' %>
+
<%= form_for @enterprise_set, :url => main_app.bulk_update_admin_enterprises_path do |f| %>
diff --git a/app/views/admin/enterprises/index.rabl b/app/views/admin/enterprises/index.rabl
new file mode 100644
index 0000000000..9e1ee893d1
--- /dev/null
+++ b/app/views/admin/enterprises/index.rabl
@@ -0,0 +1,14 @@
+collection @collection
+
+attributes :id, :name
+
+child supplied_products: :supplied_products do |product|
+ attributes :name
+ node(:supplier_name) { |p| p.supplier.andand.name }
+ node(:image_url) { |p| p.images.present? ? p.images.first.attachment.url(:mini) : nil }
+ node(:master_id) { |p| p.master.id }
+ child variants: :variants do |variant|
+ attributes :id
+ node(:label) { |v| v.options_text }
+ end
+end
diff --git a/app/views/admin/enterprises/index.rep b/app/views/admin/enterprises/index.rep
deleted file mode 100644
index beaceef8a4..0000000000
--- a/app/views/admin/enterprises/index.rep
+++ /dev/null
@@ -1,15 +0,0 @@
-r.list_of :enterprises, @collection do
- r.element :id
- r.element :name
-
- r.list_of :supplied_products do |product|
- r.element :name
- r.element :supplier_name, product.supplier.andand.name
- r.element :image_url, product.images.present? ? product.images.first.attachment.url(:mini) : nil
- r.element :master_id, product.master.id
- r.list_of :variants do |variant|
- r.element :id
- r.element :label, variant.options_text
- end
- end
-end
diff --git a/app/views/admin/json/_enterprise_relationship.rabl b/app/views/admin/json/_enterprise_relationship.rabl
new file mode 100644
index 0000000000..9be152ec5c
--- /dev/null
+++ b/app/views/admin/json/_enterprise_relationship.rabl
@@ -0,0 +1,11 @@
+object @enterprise_relationship
+
+attributes :id, :parent_id, :child_id
+
+node :parent_name do |enterprise_relationship|
+ enterprise_relationship.parent.name
+end
+
+node :child_name do |enterprise_relationship|
+ enterprise_relationship.child.name
+end
diff --git a/app/views/admin/json/_enterprise_relationships.rabl b/app/views/admin/json/_enterprise_relationships.rabl
new file mode 100644
index 0000000000..aad55b9770
--- /dev/null
+++ b/app/views/admin/json/_enterprise_relationships.rabl
@@ -0,0 +1,2 @@
+collection @enterprise_relationships
+extends "admin/json/enterprise_relationship"
diff --git a/app/views/admin/json/_enterprises.rabl b/app/views/admin/json/_enterprises.rabl
new file mode 100644
index 0000000000..29e1dc7bf6
--- /dev/null
+++ b/app/views/admin/json/_enterprises.rabl
@@ -0,0 +1,3 @@
+collection @enterprises
+
+attributes :id, :name
diff --git a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml
index 3f7d7ba578..ae0b9b7349 100644
--- a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml
+++ b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml
@@ -1,7 +1,7 @@
%td{:colspan => 3}
.exchange-select-all-variants
%label
- = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-click' => 'setExchangeVariants(exchange, incomingExchangesVariants(), exchange.select_all_variants)', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants'
+ = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, incomingExchangesVariants(), exchange.select_all_variants)', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants'
Select all
.exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle'}
diff --git a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml
index 6ad13a88a6..ac791d7fd2 100644
--- a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml
+++ b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml
@@ -2,7 +2,7 @@
%td{:colspan => 3}
.exchange-select-all-variants
%label
- = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-click' => 'setExchangeVariants(exchange, suppliedVariants(exchange.enterprise_id), exchange.select_all_variants)', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants'
+ = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, suppliedVariants(exchange.enterprise_id), exchange.select_all_variants)', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants'
Select all
.exchange-product{'ng-repeat' => 'product in enterprises[exchange.enterprise_id].supplied_products'}
diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml
new file mode 100644
index 0000000000..4a793689b7
--- /dev/null
+++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml
@@ -0,0 +1,4 @@
+= content_for :sub_menu do
+ %ul#sub_nav.inline-menu{"data-hook" => "admin_order_sub_tabs"}
+ = tab :enterprises, url: main_app.admin_enterprises_path
+ = tab :relationships, url: main_app.admin_enterprise_relationships_path, match_path: '/enterprise_relationships'
diff --git a/app/views/checkout/_order.rabl b/app/views/checkout/_order.rabl
index 9903795493..36e5a98eed 100644
--- a/app/views/checkout/_order.rabl
+++ b/app/views/checkout/_order.rabl
@@ -18,8 +18,8 @@ child current_order.ship_address => :ship_address do
end
node :shipping_methods do
- Hash[current_order.distributor.shipping_methods.collect {
- |method| [method.id, {
+ Hash[current_order.available_shipping_methods("front_end").collect { |method|
+ [method.id, {
require_ship_address: method.require_ship_address,
price: method.compute_amount(current_order).to_f,
name: method.name
diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml
new file mode 100644
index 0000000000..c1fd5ccf8e
--- /dev/null
+++ b/app/views/groups/index.html.haml
@@ -0,0 +1,44 @@
+#groups{"ng-controller" => "GroupsCtrl"}
+ :javascript
+ angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups})
+ .row.pad-top
+ .small-12.columns.text-center
+ %h1 Groups / Regions
+ %div
+ Check out our
+ %ofn-modal{title: "food groups"}
+ = render partial: "modals/groups"
+ below
+ %p
+
+ %input{type: :text,
+ "ng-model" => "query",
+ placeholder: "Search group name",
+ "ng-debounce" => "150",
+ "ofn-disable-enter" => true}
+
+ .group{"ng-repeat" => "group in Groups.groups | filter:query | orderBy:order",
+ name: "group{{group.id}}",
+ id: "group{{group.id}}"}
+ .row.pad-top{bindonce: true}
+ .small-12.columns
+ .group-hero
+ %img.group-hero-img{"bo-src" => "group.promo_image"}
+ %img.group-logo{"bo-src" => "group.logo", "bo-if" => "group.logo"}
+ %h3.group-name {{ group.name }}
+ %h5.group-description {{ group.description }}
+
+ .row.pad-top{bindonce: true}
+ .small-6.columns
+ %p {{ group.long_description }}
+ .small-6.columns
+ %h5 Our hubs & producers
+ %ul.small-block-grid-2
+ %li{"ng-repeat" => "enterprise in group.enterprises"}
+ %a{"bo-href" => "enterprise.path"} {{ enterprise.name }}
+
+ .row.group_footer
+ .small-12.columns
+ %hr
+
+= render partial: "shared/footer"
diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml
index 7d5dfb3b80..9382844265 100644
--- a/app/views/home/_fat.html.haml
+++ b/app/views/home/_fat.html.haml
@@ -2,7 +2,8 @@
.columns.small-4
%strong Shop for
%p.trans-sentence
- {{ hub.taxons | printArrayOfObjects }}
+ %img{"ng-repeat" => "taxon in hub.taxons", "bo-src" => "taxon.icon",
+ name: "{{taxon.name}}", alt: "{{taxon.name}}"}
.columns.small-4
%strong Delivery options
%ol
@@ -10,8 +11,9 @@
%li.delivery{"bo-if" => "hub.delivery"} Delivery
.columns.small-4
%strong Our producers
- %p
- Go to our shop to see our current producers
+ %ul
+ %li{"ng-repeat" => "producer in hub.producers"}
+ = render partial: "modals/producer"
.row.active_table_row.link{"ng-show" => "open()", "ng-if" => "hub.active"}
.columns.small-11
diff --git a/app/views/home/_groups.html.haml b/app/views/home/_groups.html.haml
index 0e16977f20..dd55a9bcd1 100644
--- a/app/views/home/_groups.html.haml
+++ b/app/views/home/_groups.html.haml
@@ -5,6 +5,6 @@
%h2 Groups / Regions
%h5 See all the groups & regions on the Open Food Network
%p
- %button.neutral-btn.light
+ %a.neutral-btn.light{href: "/groups"}
%i.fi-torsos-all
View groups & regions
\ No newline at end of file
diff --git a/app/views/home/_producers.html.haml b/app/views/home/_producers.html.haml
index c24f170e72..31a573d610 100644
--- a/app/views/home/_producers.html.haml
+++ b/app/views/home/_producers.html.haml
@@ -5,6 +5,6 @@
%h2 Producers
%h5 Looking for a specific producer or farmer?
%p
- %button.neutral-btn.turquoise
+ %a.neutral-btn.turquoise{href: "/producers"}
%i.fi-trees
View all producers
\ No newline at end of file
diff --git a/app/views/json/_current_hub.rabl b/app/views/json/_current_hub.rabl
index ab67cce3cc..7f615bb07a 100644
--- a/app/views/json/_current_hub.rabl
+++ b/app/views/json/_current_hub.rabl
@@ -1,2 +1,8 @@
object current_distributor
attributes :name, :id
+
+if current_distributor
+ child suppliers: :producers do
+ extends "json/producer"
+ end
+end
diff --git a/app/views/json/_enterprises.rabl b/app/views/json/_enterprises.rabl
index 97e040eca9..2f2f82c944 100644
--- a/app/views/json/_enterprises.rabl
+++ b/app/views/json/_enterprises.rabl
@@ -4,3 +4,11 @@ attributes :name, :id, :description
child :address do
extends "json/partials/address"
end
+
+node :path do |enterprise|
+ shop_enterprise_path(enterprise)
+end
+
+node :hash do |enterprise|
+ enterprise.to_param
+end
diff --git a/app/views/json/_groups.rabl b/app/views/json/_groups.rabl
new file mode 100644
index 0000000000..27325bb850
--- /dev/null
+++ b/app/views/json/_groups.rabl
@@ -0,0 +1,14 @@
+collection @groups
+attributes :id, :name, :position, :description, :long_description
+
+child enterprises: :enterprises do
+ extends 'json/enterprises'
+end
+
+node :logo do |group|
+ group.logo(:medium) if group.logo.exists?
+end
+
+node :promo_image do |group|
+ group.promo_image(:large) if group.promo_image.exists?
+end
diff --git a/app/views/json/_hubs.rabl b/app/views/json/_hubs.rabl
index a6958840bc..c118b4504f 100644
--- a/app/views/json/_hubs.rabl
+++ b/app/views/json/_hubs.rabl
@@ -2,11 +2,11 @@ collection Enterprise.visible.is_distributor
extends 'json/enterprises'
child distributed_taxons: :taxons do
- attributes :name, :id
+ extends "json/taxon"
end
child suppliers: :producers do
- attributes :name, :id
+ extends "json/producer"
end
node :pickup do |hub|
@@ -17,14 +17,6 @@ node :delivery do |hub|
not hub.shipping_methods.where(:require_ship_address => true).empty?
end
-node :path do |hub|
- shop_enterprise_path(hub)
-end
-
-node :hash do |hub|
- hub.to_param
-end
-
node :active do |hub|
@active_distributors.include?(hub)
end
diff --git a/app/views/json/_injection.html.haml b/app/views/json/_injection.html.haml
new file mode 100644
index 0000000000..95b921406b
--- /dev/null
+++ b/app/views/json/_injection.html.haml
@@ -0,0 +1,2 @@
+:javascript
+ angular.module('Darkswarm').value("#{name.to_s}", #{render "json/#{partial.to_s}"})
diff --git a/app/views/json/_producer.rabl b/app/views/json/_producer.rabl
new file mode 100644
index 0000000000..4c0efae8e1
--- /dev/null
+++ b/app/views/json/_producer.rabl
@@ -0,0 +1,5 @@
+attributes :name, :id, :description, :long_description
+
+node :promo_image do |producer|
+ producer.promo_image.url
+end
diff --git a/app/views/json/_producers.rabl b/app/views/json/_producers.rabl
index 717347e5de..95a133a182 100644
--- a/app/views/json/_producers.rabl
+++ b/app/views/json/_producers.rabl
@@ -2,7 +2,7 @@ collection @producers
extends 'json/enterprises'
child supplied_taxons: :taxons do
- attributes :name, :id
+ extends 'json/taxon'
end
child distributors: :distributors do
diff --git a/app/views/json/_taxon.rabl b/app/views/json/_taxon.rabl
new file mode 100644
index 0000000000..916abeff78
--- /dev/null
+++ b/app/views/json/_taxon.rabl
@@ -0,0 +1,5 @@
+attributes :name, :id, :permalink
+
+node :icon do |taxon|
+ taxon.icon.url
+end
diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml
index 5c2dcaee66..edeffafeed 100644
--- a/app/views/layouts/darkswarm.html.haml
+++ b/app/views/layouts/darkswarm.html.haml
@@ -15,16 +15,16 @@
= csrf_meta_tags
%body.off-canvas{"ng-app" => "Darkswarm"}
+ = inject_json "currentHub", "current_hub"
+ = inject_json "user", "current_user"
+
.off-canvas-wrap{offcanvas: true}
.inner-wrap
- = render partial: "shared/current_hub"
- = render partial: "shared/current_user"
= render partial: "shared/menu/menu"
+
= display_flash_messages
%ofn-flash
- -#= render "shared/sidebar"
-
%section{ role: "main" }
= yield
diff --git a/app/views/modals/_food_hub.html.haml b/app/views/modals/_food_hub.html.haml
index e80ffe3745..9b559d26cc 100644
--- a/app/views/modals/_food_hub.html.haml
+++ b/app/views/modals/_food_hub.html.haml
@@ -2,4 +2,4 @@
%h5 Our food hubs are the point of contact between you and the people who make your food!
%p You can search for a convenient hub by location or name. Some hubs have multiple points where you can pick-up your purchases, and some will also provide delivery options. Each food hub is a sales point with independent business operations and logisitics - so variations between hubs are to be expected.
%p You can only shop one food hub at a time.
-%a.close-reveal-modal{"ng-click" => "cancel()"} ×
\ No newline at end of file
+%a.close-reveal-modal{"ng-click" => "$close()"} ×
diff --git a/app/views/modals/_groups.html.haml b/app/views/modals/_groups.html.haml
new file mode 100644
index 0000000000..80e210c98a
--- /dev/null
+++ b/app/views/modals/_groups.html.haml
@@ -0,0 +1,4 @@
+%h2 Groups / Regions
+%p These are the organisations and relationships between hubs which make up the Open Food Network.
+%p Some groups are clustered by location or council, others by non-geographic similarities.
+%a.close-reveal-modal{"ng-click" => "cancel()"} ×
\ No newline at end of file
diff --git a/app/views/modals/_learn_more.html.haml b/app/views/modals/_learn_more.html.haml
index 870d5fe0bc..4cb0534c6e 100644
--- a/app/views/modals/_learn_more.html.haml
+++ b/app/views/modals/_learn_more.html.haml
@@ -6,4 +6,4 @@
%h5 Learn more
%p If you want to learn more about the Open Food Network, how it works, and get involved, check out:
%a.button.neutral-btn.dark{:href => "http://www.openfoodnetwork.org" , :target => "_blank" } Open Food Network
-%a.close-reveal-modal{"ng-click" => "cancel()"} ×
+%a.close-reveal-modal{"ng-click" => "$close()"} ×
diff --git a/app/views/modals/_producer.html.haml b/app/views/modals/_producer.html.haml
new file mode 100644
index 0000000000..13f8026195
--- /dev/null
+++ b/app/views/modals/_producer.html.haml
@@ -0,0 +1,39 @@
+%ofn-modal{title: "{{producer.name}}"}
+ .row
+ .columns.small-12.producer-hero
+ %img.producer-hero-img{"ng-src" => "{{producer.promo_image}}"}
+ / Will - scale large images down to 1200px wide, crop in to img aspect ratio 60W:13H
+ %h3.producer-name {{ producer.name }}
+ .row
+ .columns.small-12.large-6{"ng-bind-html" => "producer.long_description"}
+ .columns.small-12.large-6
+ %img.producer-logo{"ng-src" => "{{producer.logo}}", "ng-if" => "producer.logo"}
+ %h4 Stay in touch with {{ producer.name }}
+ %ul.small-block-grid-1{bindonce: true}
+ %li{"ng-if" => "producer.website"}
+ %a{"ng-href" => "http://{{producer.website | stripUrl}}", target: "_blank" }
+ %i.fi-web
+ {{ producer.website | stripUrl }}
+
+ %li{"ng-if" => "producer.twitter"}
+ %a{"ng-href" => "http://twitter.com/{{producer.twitter}}", target: "_blank"}
+ %i.fi-social-twitter
+ {{ producer.twitter }}
+
+ %li{"ng-if" => "producer.facebook"}
+ %a{"ng-href" => "http://{{producer.facebook | stripUrl}}", target: "_blank"}
+ %i.fi-social-facebook
+ {{ producer.facebook | stripUrl }}
+
+ %li{"ng-if" => "producer.linkedin"}
+ %a{"ng-href" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"}
+ %i.fi-social-linkedin
+ {{ producer.linkedin | stripUrl }}
+
+ %li{"ng-if" => "producer.instagram"}
+ %a{"ng-href" => "http://instagram.com/{{producer.instagram}}", target: "_blank"}
+ %i.fi-social-instagram
+ {{ producer.instagram }}
+
+
+ %a.close-reveal-modal{"ng-click" => "$close()"} ×
diff --git a/app/views/modals/_product.html.haml b/app/views/modals/_product.html.haml
new file mode 100644
index 0000000000..9c071b51eb
--- /dev/null
+++ b/app/views/modals/_product.html.haml
@@ -0,0 +1,10 @@
+%ofn-modal{title: "{{product.name}}"}
+ .row
+ .columns.small-12.large-6
+ %img.product-img{"ng-src" => "{{product.master.images[0].large_url}}", "ng-if" => "product.master.images[0]"}
+ .columns.small-12.large-6
+ %h2
+ %img{"ng-src" => "{{product.primary_taxon.icon}}"}
+ {{product.name}}
+ %p {{product.description}}
+ %a.close-reveal-modal{"ng-click" => "$close()"} ×
diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml
index 45b3bf8dd0..3313db6552 100644
--- a/app/views/producers/_fat.html.haml
+++ b/app/views/producers/_fat.html.haml
@@ -5,7 +5,7 @@
{{ producer.taxons | printArrayOfObjects }}
.columns.small-8
%strong About us
- %p.trans-sentence
+ %p
{{ producer.description }}
.row.active_table_row.link{"ng-show" => "open()", "ng-repeat" => "hub in producer.distributors"}
diff --git a/app/views/shared/_current_hub.haml b/app/views/shared/_current_hub.haml
deleted file mode 100644
index 5a0ff7c564..0000000000
--- a/app/views/shared/_current_hub.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-:javascript
- angular.module('Darkswarm').value('currentHub', #{render "json/current_hub"})
diff --git a/app/views/shared/_current_user.haml b/app/views/shared/_current_user.haml
deleted file mode 100644
index 9745a71313..0000000000
--- a/app/views/shared/_current_user.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-:javascript
- angular.module('Darkswarm').value('user', #{render "json/current_user"})
diff --git a/app/views/shared/menu/_large_menu.html.haml b/app/views/shared/menu/_large_menu.html.haml
index 328d46ca85..7a72b80f60 100644
--- a/app/views/shared/menu/_large_menu.html.haml
+++ b/app/views/shared/menu/_large_menu.html.haml
@@ -21,10 +21,10 @@
%span.nav-primary Producers
%li.divider
%li
- %a{href: ""}
+ %a{href: main_app.groups_path}
%span.nav-primary Groups
%li.divider
- - if spree_current_user.andand.has_spree_role? 'admin'
+ - if admin_user? or enterprise_user?
%li
%a{href: spree.admin_path}
%span.nav-primary Admin
diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml
index a33cb85c11..5e9f22c597 100644
--- a/app/views/shared/menu/_mobile_menu.html.haml
+++ b/app/views/shared/menu/_mobile_menu.html.haml
@@ -15,7 +15,7 @@
%ul.off-canvas-list
%li= link_to image_tag("ofn_logo_small.png"), root_path
- - if spree_current_user.andand.has_spree_role? 'admin'
+ - if admin_user? or enterprise_user?
%li
%a{href: spree.admin_path}
%span.nav-primary Admin
@@ -36,17 +36,14 @@
%li
%a{href: root_path + "#/#hubs"}
%span.nav-primary Hubs
-
%li
%a{href: ""}
%span.nav-primary Map
-
%li
%a{href: main_app.producers_path}
%span.nav-primary Producers
-
%li
- %a{href: ""}
+ %a{href: main_app.groups_path}
%span.nav-primary Groups
diff --git a/app/views/shared/unauthorized.html.haml b/app/views/shared/unauthorized.html.haml
new file mode 100644
index 0000000000..fcf3f3bf27
--- /dev/null
+++ b/app/views/shared/unauthorized.html.haml
@@ -0,0 +1 @@
+Unauthorized
diff --git a/app/views/shop/_products.html.haml b/app/views/shop/_products.html.haml
deleted file mode 100644
index 7ed6606717..0000000000
--- a/app/views/shop/_products.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%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-debounce" => "150",
- "ng-keypress" => "searchKeypress($event)"}
- %input.button.right{type: :submit, value: "Add to Cart"}
-
- %table
- %thead
- %th.name Item
- %th.notes Notes
- %th.variant Unit
- %th.quantity QTY
- %th.bulk Bulk
- %th.price.text-right Price
- %tbody{"ng-show" => "data.loading"}
- %tr
- %td{colspan: 6}
- %h3.text-center Loading Products
- %tbody{"ng-repeat" => "product in data.products | filter:query | limitTo: limit track by product.id"}
- %tr{"class" => "product product-{{ product.id }}"}
-
- %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{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",
- "ng-show" => "product.show_variants",
- "ng-click" => "product.show_variants = !product.show_variants"}
-
- %img.expand{src: "/assets/expand.png",
- "ng-show" => "!product.show_variants",
- "ng-click" => "product.show_variants = !product.show_variants"}
- %td
- %span{"ng-show" => "(product.variants.length == 0)"}
- %input{type: :number,
- value: nil,
- min: 0,
- "ofn-disable-scroll" => true,
- max: "{{product.on_demand && 9999 || product.count_on_hand }}",
- name: "variants[{{product.master.id}}]",
- id: "variants_{{product.master.id}}",
- "ng-model" => "product.quantity"}
-
- %td.group_buy
- %span{"ng-show" => "product.group_buy && (product.variants.length == 0)"}
- %input{type: :number,
- min: 0,
- "ofn-disable-scroll" => true,
- max: "{{product.on_demand && 9999 || product.count_on_hand }}",
- name: "variant_attributes[{{product.master.id}}][max_quantity]",
- "ng-model" => "product.max_quantity"}
-
- %td.price.text-right{bindonce: ""}
- %small{"ng-show" => "(product.variants.length > 0)"} from
- {{ productPrice(product) | currency }}
-
- %tr.product-description{bindonce: ""}
- %td{colspan: 2}{{ product.notes | truncate:80 }}
-
- %tr.variant{"ng-repeat" => "variant in product.variants", "ng-if" => "product.show_variants"}
- = render partial: "shop/variant"
-
- %input.button.right{type: :submit, value: "Add to Cart"}
-
-
diff --git a/app/views/shop/_variant.html.haml b/app/views/shop/_variant.html.haml
deleted file mode 100644
index 15d5485090..0000000000
--- a/app/views/shop/_variant.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-%td
-%td.notes
-%td{bindonce: ""} {{variant.options_text}}
-%td
- %input{type: :number,
- value: nil,
- min: 0,
- "ofn-disable-scroll" => true,
- max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
- name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}",
- "ng-model" => "variant.quantity"}
-%td.group_buy
- %span{"ng-show" => "product.group_buy"}
- %input{type: :number,
- min: 0,
- "ofn-disable-scroll" => true,
- max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
- name: "variant_attributes[{{variant.id}}][max_quantity]",
- "ng-model" => "variant.max_quantity"}
-%td.price.text-right{bindonce: ""}
- {{ variant.price | currency }}
diff --git a/app/views/shop/products.rabl b/app/views/shop/products.rabl
index c92a1afaa0..b1567a311a 100644
--- a/app/views/shop/products.rabl
+++ b/app/views/shop/products.rabl
@@ -1,10 +1,6 @@
collection @products
attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy
-node :show_variants do
- true
-end
-
node do |product|
{
notes: strip_tags(product.notes),
@@ -14,7 +10,18 @@ node do |product|
end
child :supplier => :supplier do
- attributes :id, :name, :description
+ attributes :id, :name, :description, :long_description, :website, :instagram, :facebook, :linkedin, :twitter
+
+ node :logo do |supplier|
+ supplier.logo(:medium) if supplier.logo.exists?
+ end
+ node :promo_image do |supplier|
+ supplier.promo_image(:large) if supplier.promo_image.exists?
+ end
+end
+
+child :primary_taxon => :primary_taxon do
+ extends 'json/taxon'
end
child :master => :master do
@@ -22,7 +29,8 @@ child :master => :master do
child :images => :images do
attributes :id, :alt
node do |img|
- {:small_url => img.attachment.url(:small, false)}
+ {:small_url => img.attachment.url(:small, false),
+ :large_url => img.attachment.url(:large, false)}
end
end
end
diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml
new file mode 100644
index 0000000000..5a3af9684e
--- /dev/null
+++ b/app/views/shop/products/_form.html.haml
@@ -0,0 +1,29 @@
+%products.small-12.columns{"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
+
+ .row
+ .small-6.columns
+ %input#search.text{"ng-model" => "query",
+ placeholder: "Search",
+ "ng-debounce" => "150",
+ "ofn-disable-enter" => true}
+ .small-6.columns
+ %input.button.primary.right{type: :submit, value: "Add to Cart"}
+
+ %div{bindonce: true}
+ %product{"ng-controller" => "ProductNodeCtrl",
+ "ng-repeat" => "product in data.products | filter:query | orderBy:ordering.order | limitTo: limit track by product.id"}
+ %div
+ = render partial: "shop/products/summary"
+
+ %div{"bo-if" => "hasVariants"}
+ = render partial: "shop/products/variants"
+
+ .variant.row{"bo-if" => "!hasVariants"}
+ = render partial: "shop/products/master"
+
+ .row
+ .small-12.columns
+ %input.button.primary.right.add_to_cart{type: :submit, value: "Add to Cart"}
diff --git a/app/views/shop/products/_master.html.haml b/app/views/shop/products/_master.html.haml
new file mode 100644
index 0000000000..7398912bda
--- /dev/null
+++ b/app/views/shop/products/_master.html.haml
@@ -0,0 +1,39 @@
+.small-1.column
+ %span.bulk{"bo-if" => "product.group_buy"} bulk
+
+
+.small-4.columns
+ ({{ product.master.options_text }})
+
+-# WITHOUT GROUP BUY
+.small-5.columns{"bo-if" => "!product.group_buy"}
+ %input{type: :number,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{product.on_demand && 9999 || product.count_on_hand }}",
+ name: "variants[{{product.master.id}}]",
+ id: "variants_{{product.master.id}}",
+ "ng-model" => "product.quantity"}
+
+-# WITH GROUP BUY
+.small-2.columns{"bo-if" => "product.group_buy"}
+ %input{type: :number,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{product.on_demand && 9999 || product.count_on_hand }}",
+ name: "variants[{{product.master.id}}]",
+ id: "variants_{{product.master.id}}",
+ "ng-model" => "product.quantity"}
+ (min)
+
+.small-3.columns{"bo-if" => "product.group_buy"}
+ %input{type: :number,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{product.on_demand && 9999 || product.count_on_hand }}",
+ name: "variant_attributes[{{product.master.id}}][max_quantity]",
+ "ng-model" => "product.max_quantity"}
+ (max)
+
+.small-2.columns.text-right
+ {{ product.price | currency }}
diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml
new file mode 100644
index 0000000000..e41c6ef440
--- /dev/null
+++ b/app/views/shop/products/_summary.html.haml
@@ -0,0 +1,17 @@
+.row.summary
+ .small-1.column
+ %img{"bo-src" => "product.master.images[0].small_url"}
+
+ .small-4.columns.summary-header
+ %img{"bo-src" => "product.primary_taxon.icon",
+ "ng-click" => "ordering.order = 'primary_taxon.name'",
+ name: "{{product.primary_taxon.name}}"}
+ = render partial: "modals/product"
+
+ .small-5.columns.summary-header
+ = render partial: "modals/producer"
+
+ .small-2.columns.summary-price.text-right.price
+ %span{"ng-if" => "hasVariants"}
+ %em from
+ {{ price() | currency }}
diff --git a/app/views/shop/products/_variants.html.haml b/app/views/shop/products/_variants.html.haml
new file mode 100644
index 0000000000..14ff8eff24
--- /dev/null
+++ b/app/views/shop/products/_variants.html.haml
@@ -0,0 +1,42 @@
+.row.variants{bindonce: true,
+ "ng-repeat" => "variant in product.variants"}
+
+ .small-1.column
+ %span.bulk{"bo-if" => "product.group_buy"} bulk
+
+
+ .small-4.columns
+ {{ variant.options_text }}
+
+ -# WITHOUT GROUP BUY
+ .small-5.columns{"bo-if" => "!product.group_buy"}
+ %input{type: :number,
+ value: nil,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
+ name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}",
+ "bo-model" => "variant.quantity"}
+
+ -# WITH GROUP BUY
+ .small-2.columns{"bo-if" => "product.group_buy"}
+ %input{type: :number,
+ value: nil,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
+ name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}",
+ "bo-model" => "variant.quantity"}
+ (min)
+
+ .small-3.columns{"bo-if" => "product.group_buy"}
+ %input{type: :number,
+ min: 0,
+ "ofn-disable-scroll" => true,
+ max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
+ name: "variant_attributes[{{variant.id}}][max_quantity]",
+ "ng-model" => "variant.max_quantity"}
+ (max)
+
+ .small-2.columns.text-right.price
+ {{ variant.price | currency }}
diff --git a/app/views/shop/show.html.haml b/app/views/shop/show.html.haml
index ecfcc8e4e3..d4b36a5e29 100644
--- a/app/views/shop/show.html.haml
+++ b/app/views/shop/show.html.haml
@@ -1,17 +1,20 @@
%shop.darkswarm
- content_for :order_cycle_form do
- %strong.avenir Ready for
- %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
- "ng-change" => "changeOrderCycle()",
- "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}",
- "popover-placement" => "bottom", "popover" => "When do you want to get your order?", "popover-trigger" => "openTrigger"}
+ %div{"ng-controller" => "OrderCycleChangeCtrl"}
+ %strong.avenir Ready for
+ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
+ "ng-change" => "changeOrderCycle()",
+ "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}",
+ "popover-placement" => "bottom", "popover" => "When do you want to get your order?", "popover-trigger" => "openTrigger"}
- %closing{"ng-if" => "OrderCycle.selected()"}
- Orders close
- %strong {{ OrderCycle.orders_close_at() | date_in_words }}
+ %closing{"ng-if" => "OrderCycle.selected()"}
+ Orders close
+ %strong {{ OrderCycle.orders_close_at() | date_in_words }}
= render partial: "shopping_shared/details"
- %products.row
- = render partial: "shop/products"
+ .row
+ = render partial: "shop/products/form"
+
+= render partial: "shared/footer"
diff --git a/app/views/shopping_shared/_details.html.haml b/app/views/shopping_shared/_details.html.haml
index 37edfa3c16..e152be43c0 100644
--- a/app/views/shopping_shared/_details.html.haml
+++ b/app/views/shopping_shared/_details.html.haml
@@ -1,5 +1,3 @@
-= render partial: "shopping_shared/modals"
-
%navigation
%distributor.details.row
#distributor_title
diff --git a/app/views/shopping_shared/_groups.html.haml b/app/views/shopping_shared/_groups.html.haml
index a218d6aeb5..e9ebb219b3 100644
--- a/app/views/shopping_shared/_groups.html.haml
+++ b/app/views/shopping_shared/_groups.html.haml
@@ -1,10 +1,5 @@
-.content#groups
+.content
%ul
- for group in current_distributor.groups
%li
- %h4= group.name
- %ul
- - for sibling in group.enterprises.except(current_distributor)
- %li
- %a{"data-reveal-id" => "sibling_details_#{sibling.id}", "data-reveal" => ""}
- = sibling.name
+ %a{href: main_app.groups_path(anchor: "#/#group#{group.id}")}= group.name
diff --git a/app/views/shopping_shared/_modals.html.haml b/app/views/shopping_shared/_modals.html.haml
deleted file mode 100644
index ad92f1957f..0000000000
--- a/app/views/shopping_shared/_modals.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- for producer in current_producers
- .reveal-modal{id: "producer_details_#{producer.id}", "data-reveal" => ""}
- .row
- - if producer.logo.exists?
- .large-1.columns
- %img.left{src: producer.logo.url(:thumb)}
- .large-11.columns
- %h2
- = producer.name
- .row
- .large-8.columns
- = producer.long_description.andand.html_safe
-
- - if producer.promo_image.exists?
- .large-4.columns
- %img.about.right{src: producer.promo_image.url(:large)}
- %a.close-reveal-modal ×
-
-
-- for group in current_distributor.groups
- - for sibling in group.enterprises.except(current_distributor)
- .reveal-modal{id: "sibling_details_#{sibling.id}", "data-reveal" => ""}
- .row
- - if sibling.logo.exists?
- .large-1.columns
- %img.left{src: sibling.logo.url(:thumb)}
- .large-11.columns
- %h2
- = sibling.name
- .row
- .large-8.columns
- = sibling.long_description.andand.html_safe
- - if sibling.promo_image.exists?
- .large-4.columns
- %img.about.right{src: sibling.promo_image.url(:large)}
- %a.close-reveal-modal ×
diff --git a/app/views/shopping_shared/_order_cycles.html.haml b/app/views/shopping_shared/_order_cycles.html.haml
index 91517e0c1c..195389df91 100644
--- a/app/views/shopping_shared/_order_cycles.html.haml
+++ b/app/views/shopping_shared/_order_cycles.html.haml
@@ -14,5 +14,3 @@
- else
%form.custom
= yield :order_cycle_form
-
-
diff --git a/app/views/shopping_shared/_producers.html.haml b/app/views/shopping_shared/_producers.html.haml
index 32f09869e8..584d75ff35 100644
--- a/app/views/shopping_shared/_producers.html.haml
+++ b/app/views/shopping_shared/_producers.html.haml
@@ -1,6 +1,4 @@
-.content#producers
+.content#producers{"ng-controller" => "ProducersTabCtrl"}
%ul
- - for producer in current_producers
- %li
- %a{"data-reveal-id" => "producer_details_#{producer.id}", "data-reveal" => ""}
- = producer.name
+ %li{"ng-repeat" => "producer in CurrentHub.producers"}
+ = render partial: "modals/producer"
diff --git a/app/views/spree/admin/products/_primary_taxon_form.html.haml b/app/views/spree/admin/products/_primary_taxon_form.html.haml
new file mode 100644
index 0000000000..3cd7bdc931
--- /dev/null
+++ b/app/views/spree/admin/products/_primary_taxon_form.html.haml
@@ -0,0 +1,5 @@
+= f.field_container :primary_taxon_id do
+ = f.label :primary_taxon_id, t(:product_category)
+ %br
+ = f.collection_select(:primary_taxon_id, Spree::Taxon.all, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
+ = f.error_message_on :primary_taxon_id
diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml
index ee74fd9662..fa295adb47 100644
--- a/app/views/spree/admin/products/bulk_edit.html.haml
+++ b/app/views/spree/admin/products/bulk_edit.html.haml
@@ -132,7 +132,7 @@
%td{ 'ng-show' => 'columns.on_hand.visible' }
%span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' }
%input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' }
- %td{ 'ng-show' => 'columns.taxons.visible' }
+ %td{ 'ng-if' => 'columns.taxons.visible' }
%input.fullwidth{ :type => 'text', 'ng-model' => 'product.taxon_ids', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'taxon_ids' }
%td{ 'ng-show' => 'columns.available_on.visible' }
%input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" }
diff --git a/app/views/spree/orders/_form.html.haml b/app/views/spree/orders/_form.html.haml
index 9366e0025d..bec495a458 100644
--- a/app/views/spree/orders/_form.html.haml
+++ b/app/views/spree/orders/_form.html.haml
@@ -19,4 +19,27 @@
= order_form.fields_for :line_items do |item_form|
= render :partial => 'line_item', :locals => { :variant => item_form.object.variant, :line_item => item_form.object, :item_form => item_form }
+ %tfoot#edit-cart
+ %tr
+ %td
+ Product
+ \:
+ %span.order-total.item-total= number_to_currency @order.item_total
+ %td
+ Distribution
+ \:
+ %span.order-total.distribution-total= order_distribution_subtotal(@order)
+ %td
+ %td
+ = button_tag :class => 'neutral-btn dark expand small', :id => 'update-button' do
+ %i.fi-refresh
+ = t(:update)
+ %td
+ %h5.order-total.grand-total= @order.display_total
+ %td#empty-cart.text-center
+ %span#clear_cart_link{"data-hook" => ""}
+ = link_to "Empty cart", empty_cart_path, method: :put, :class => 'not-bold small'
+ -#= form_tag empty_cart_path, :method => :put do
+ -#= submit_tag t(:empty_cart), :class => 'button alert expand small'
+
= render "spree/orders/adjustments" unless @order.adjustments.eligible.blank?
diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml
index bb7054655f..6986caf100 100644
--- a/app/views/spree/orders/_line_item.html.haml
+++ b/app/views/spree/orders/_line_item.html.haml
@@ -21,7 +21,7 @@
%td.cart-item-total{"data-hook" => "cart_item_total"}
= line_item.display_amount.to_html unless line_item.quantity.nil?
- %td.cart-item-delete{"data-hook" => "cart_item_delete"}
+ %td.cart-item-delete.text-center{"data-hook" => "cart_item_delete"}
{{ quantity }}
= link_to image_tag('icons/delete.png'), '#', :class => 'delete',
:id => "delete_#{dom_id(line_item)}"
diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml
index 06b82d5169..f0c6b95a78 100644
--- a/app/views/spree/orders/edit.html.haml
+++ b/app/views/spree/orders/edit.html.haml
@@ -24,35 +24,18 @@
.row
= render :partial => 'form', :locals => { :order_form => order_form }
- #subtotal.row{'data-hook' => ""}
- .columns.large-5
- %h5
- Product
- \:
- %span.order-total.item-total= number_to_currency @order.item_total
- .columns.large-4
- %h5
- Distribution
- \:
- %span.order-total.distribution-total= order_distribution_subtotal(@order)
- .columns.large-3
- %h4
- Cart Total
- \:
- %span.order-total.grand-total= @order.display_total
.links{'data-hook' => "cart_buttons"}
.row
- #empty-cart.columns.large-8{"data-hook" => ""}
- = form_tag empty_cart_path, :method => :put do
- #clear_cart_link{"data-hook" => ""}
- = link_to "Continue Shopping", main_app.shop_path, class: "button secondary"
- = t(:or)
- = submit_tag t(:empty_cart), :class => 'button secondary'
-
+ .columns.large-8{"data-hook" => ""}
+
+ %a.button.large.secondary{href: main_app.shop_path}
+ %i.fi-arrow-left
+ Continue shopping
.columns.large-4.text-right
- = button_tag :class => 'secondary', :id => 'update-button' do
- = t(:update)
- = t(:or)
- = link_to "Checkout", main_app.checkout_path, class: "button checkout primary", id: "checkout-link"
+ %a#checkout-link.button.large.primary{href: main_app.checkout_path}
+ Checkout
+ %i.fi-arrow-right
+
+= render partial: "shared/footer"
diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml
index 559a0227f1..b6ff00925d 100644
--- a/app/views/spree/shared/_order_details.html.haml
+++ b/app/views/spree/shared/_order_details.html.haml
@@ -16,8 +16,6 @@
.columns.large-2
%h6
= t(:shipping_method)
- \:
- = link_to "(#{t(:edit)})", checkout_state_path(:delivery) unless @order.completed?
.delivery
= order.shipping_method.name
.columns.large-4
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
new file mode 100644
index 0000000000..c40b9a4130
--- /dev/null
+++ b/config/initializers/paperclip.rb
@@ -0,0 +1,3 @@
+Paperclip::Attachment.default_options[:convert_options] = {
+ all: "-auto-orient"
+}
diff --git a/config/routes.rb b/config/routes.rb
index e4868d777c..973cb288cc 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,12 +1,15 @@
Openfoodnetwork::Application.routes.draw do
root :to => 'home#index'
+ get "/#/login", to: "home#index", as: :spree_login
+
resource :shop, controller: "shop" do
get :products
post :order_cycle
get :order_cycle
end
+ resources :groups
resources :producers
get '/checkout', :to => 'checkout#edit' , :as => :checkout
@@ -38,6 +41,8 @@ Openfoodnetwork::Application.routes.draw do
post :bulk_update, :on => :collection, :as => :bulk_update
end
+ resources :enterprise_relationships
+
resources :enterprise_fees do
post :bulk_update, :on => :collection, :as => :bulk_update
end
diff --git a/db/migrate/20140402033428_add_foreign_keys.rb b/db/migrate/20140402033428_add_foreign_keys.rb
index f072127158..fce1ffd009 100644
--- a/db/migrate/20140402033428_add_foreign_keys.rb
+++ b/db/migrate/20140402033428_add_foreign_keys.rb
@@ -32,6 +32,11 @@ class AddForeignKeys < ActiveRecord::Migration
say "Destroying #{orphaned_exchange_variants.count} orphaned ExchangeVariants (of total #{ExchangeVariant.count})"
orphaned_exchange_variants.destroy_all
+ # Remove orphaned ExchangeFee records
+ orphaned_exchange_fees = ExchangeFee.joins('LEFT OUTER JOIN enterprise_fees ON enterprise_fees.id=exchange_fees.enterprise_fee_id').where('enterprise_fees.id IS NULL')
+ say "Destroying #{orphaned_exchange_fees.count} orphaned ExchangeFees (of total #{ExchangeFee.count})"
+ orphaned_exchange_fees.destroy_all
+
# Remove orphaned Spree::InventoryUnits
orphaned_inventory_units = Spree::InventoryUnit.joins('LEFT OUTER JOIN spree_variants ON spree_variants.id=spree_inventory_units.variant_id').where('spree_variants.id IS NULL')
say "Destroying #{orphaned_inventory_units.count} orphaned InventoryUnits (of total #{Spree::InventoryUnit.count})"
diff --git a/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb b/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb
new file mode 100644
index 0000000000..bd67cb92f0
--- /dev/null
+++ b/db/migrate/20140516042552_add_attachment_promo_image_to_enterprise_group.rb
@@ -0,0 +1,15 @@
+class AddAttachmentPromoImageToEnterpriseGroup < ActiveRecord::Migration
+ def self.up
+ add_column :enterprise_groups, :promo_image_file_name, :string
+ add_column :enterprise_groups, :promo_image_content_type, :string
+ add_column :enterprise_groups, :promo_image_file_size, :integer
+ add_column :enterprise_groups, :promo_image_updated_at, :datetime
+ end
+
+ def self.down
+ remove_column :enterprise_groups, :promo_image_file_name
+ remove_column :enterprise_groups, :promo_image_content_type
+ remove_column :enterprise_groups, :promo_image_file_size
+ remove_column :enterprise_groups, :promo_image_updated_at
+ end
+end
diff --git a/db/migrate/20140516044750_add_fields_to_groups.rb b/db/migrate/20140516044750_add_fields_to_groups.rb
new file mode 100644
index 0000000000..a50ef1db14
--- /dev/null
+++ b/db/migrate/20140516044750_add_fields_to_groups.rb
@@ -0,0 +1,6 @@
+class AddFieldsToGroups < ActiveRecord::Migration
+ def change
+ add_column :enterprise_groups, :description, :text
+ add_column :enterprise_groups, :long_description, :text
+ end
+end
diff --git a/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb b/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb
new file mode 100644
index 0000000000..525bccb63e
--- /dev/null
+++ b/db/migrate/20140516045323_add_attachment_logo_to_enterprise_group.rb
@@ -0,0 +1,15 @@
+class AddAttachmentLogoToEnterpriseGroup < ActiveRecord::Migration
+ def self.up
+ add_column :enterprise_groups, :logo_file_name, :string
+ add_column :enterprise_groups, :logo_content_type, :string
+ add_column :enterprise_groups, :logo_file_size, :integer
+ add_column :enterprise_groups, :logo_updated_at, :datetime
+ end
+
+ def self.down
+ remove_column :enterprise_groups, :logo_file_name
+ remove_column :enterprise_groups, :logo_content_type
+ remove_column :enterprise_groups, :logo_file_size
+ remove_column :enterprise_groups, :logo_updated_at
+ end
+end
diff --git a/db/migrate/20140522015012_add_social_media_to_enterprises.rb b/db/migrate/20140522015012_add_social_media_to_enterprises.rb
new file mode 100644
index 0000000000..9ca88cf0dc
--- /dev/null
+++ b/db/migrate/20140522015012_add_social_media_to_enterprises.rb
@@ -0,0 +1,7 @@
+class AddSocialMediaToEnterprises < ActiveRecord::Migration
+ def change
+ add_column :enterprises, :facebook, :string
+ add_column :enterprises, :instagram, :string
+ add_column :enterprises, :linkedin, :string
+ end
+end
diff --git a/db/migrate/20140522044009_add_primary_taxon_to_products.rb b/db/migrate/20140522044009_add_primary_taxon_to_products.rb
new file mode 100644
index 0000000000..f6ff3cfe96
--- /dev/null
+++ b/db/migrate/20140522044009_add_primary_taxon_to_products.rb
@@ -0,0 +1,7 @@
+class AddPrimaryTaxonToProducts < ActiveRecord::Migration
+ def change
+ add_column :spree_products, :primary_taxon_id, :integer
+ add_index :spree_products, :primary_taxon_id
+ add_foreign_key :spree_products, :spree_taxons, column: :primary_taxon_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cacc8fc793..6005fd7cb5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20140514044959) do
+ActiveRecord::Schema.define(:version => 20140522044009) do
create_table "adjustment_metadata", :force => true do |t|
t.integer "adjustment_id"
@@ -182,9 +182,19 @@ ActiveRecord::Schema.define(:version => 20140514044959) do
add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id"
create_table "enterprise_groups", :force => true do |t|
- t.string "name"
- t.boolean "on_front_page"
- t.integer "position"
+ t.string "name"
+ t.boolean "on_front_page"
+ t.integer "position"
+ t.string "promo_image_file_name"
+ t.string "promo_image_content_type"
+ t.integer "promo_image_file_size"
+ t.datetime "promo_image_updated_at"
+ t.text "description"
+ t.text "long_description"
+ t.string "logo_file_name"
+ t.string "logo_content_type"
+ t.integer "logo_file_size"
+ t.datetime "logo_updated_at"
end
create_table "enterprise_groups_enterprises", :id => false, :force => true do |t|
@@ -240,6 +250,9 @@ ActiveRecord::Schema.define(:version => 20140514044959) do
t.integer "promo_image_file_size"
t.datetime "promo_image_updated_at"
t.boolean "visible", :default => true
+ t.string "facebook"
+ t.string "instagram"
+ t.string "linkedin"
end
add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id"
@@ -668,6 +681,7 @@ ActiveRecord::Schema.define(:version => 20140514044959) do
t.float "variant_unit_scale"
t.string "variant_unit_name"
t.text "notes"
+ t.integer "primary_taxon_id"
end
add_index "spree_products", ["available_on"], :name => "index_products_on_available_on"
@@ -675,6 +689,7 @@ ActiveRecord::Schema.define(:version => 20140514044959) do
add_index "spree_products", ["name"], :name => "index_products_on_name"
add_index "spree_products", ["permalink"], :name => "index_products_on_permalink"
add_index "spree_products", ["permalink"], :name => "permalink_idx_unique", :unique => true
+ add_index "spree_products", ["primary_taxon_id"], :name => "index_spree_products_on_primary_taxon_id"
create_table "spree_products_promotion_rules", :id => false, :force => true do |t|
t.integer "product_id"
@@ -1079,6 +1094,7 @@ ActiveRecord::Schema.define(:version => 20140514044959) do
add_foreign_key "spree_products", "enterprises", name: "spree_products_supplier_id_fk", column: "supplier_id"
add_foreign_key "spree_products", "spree_shipping_categories", name: "spree_products_shipping_category_id_fk", column: "shipping_category_id"
add_foreign_key "spree_products", "spree_tax_categories", name: "spree_products_tax_category_id_fk", column: "tax_category_id"
+ add_foreign_key "spree_products", "spree_taxons", name: "spree_products_primary_taxon_id_fk", column: "primary_taxon_id"
add_foreign_key "spree_products_promotion_rules", "spree_products", name: "spree_products_promotion_rules_product_id_fk", column: "product_id"
add_foreign_key "spree_products_promotion_rules", "spree_promotion_rules", name: "spree_products_promotion_rules_promotion_rule_id_fk", column: "promotion_rule_id"
diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb
index dc5056d53a..6556223dac 100644
--- a/lib/open_food_network/order_cycle_form_applicator.rb
+++ b/lib/open_food_network/order_cycle_form_applicator.rb
@@ -5,8 +5,10 @@ module OpenFoodNetwork
# translation is more a responsibility of Angular, so I'd be inclined to refactor this class to move
# as much as possible (if not all) of its logic into Angular.
class OrderCycleFormApplicator
- def initialize(order_cycle)
+ # The applicator will only touch exchanges where a permitted enterprise is the participant
+ def initialize(order_cycle, permitted_enterprises)
@order_cycle = order_cycle
+ @permitted_enterprises = permitted_enterprises
end
def go!
@@ -56,19 +58,25 @@ module OpenFoodNetwork
def add_exchange(sender_id, receiver_id, incoming, attrs={})
attrs = attrs.reverse_merge(:sender_id => sender_id, :receiver_id => receiver_id, :incoming => incoming)
- exchange = @order_cycle.exchanges.create! attrs
- @touched_exchanges << exchange
+ exchange = @order_cycle.exchanges.build attrs
+
+ if permission_for exchange
+ exchange.save!
+ @touched_exchanges << exchange
+ end
end
def update_exchange(sender_id, receiver_id, incoming, attrs={})
exchange = @order_cycle.exchanges.where(:sender_id => sender_id, :receiver_id => receiver_id, :incoming => incoming).first
- exchange.update_attributes!(attrs)
- @touched_exchanges << exchange
+ if permission_for exchange
+ exchange.update_attributes!(attrs)
+ @touched_exchanges << exchange
+ end
end
def destroy_untouched_exchanges
- untouched_exchanges.each { |exchange| exchange.destroy }
+ with_permission(untouched_exchanges).each(&:destroy)
end
def untouched_exchanges
@@ -76,6 +84,14 @@ module OpenFoodNetwork
@order_cycle.exchanges.reject { |ex| touched_exchange_ids.include? ex.id }
end
+ def with_permission(exchanges)
+ exchanges.select { |ex| permission_for(ex) }
+ end
+
+ def permission_for(exchange)
+ @permitted_enterprises.include? exchange.participant
+ end
+
def exchange_variant_ids(exchange)
exchange[:variants].select { |k, v| v }.keys.map { |k| k.to_i }
diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake
new file mode 100644
index 0000000000..323c679fb0
--- /dev/null
+++ b/lib/tasks/users.rake
@@ -0,0 +1,86 @@
+require 'csv'
+
+namespace :openfoodnetwork do
+
+ namespace :dev do
+ desc 'export users to CSV'
+ task export_users: :environment do
+ CSV.open('db/users.csv', 'wb') do |csv|
+ csv << header
+ users.each do |user|
+ csv << row(user)
+ end
+ end
+ end
+
+
+ desc 'import users from CSV'
+ task import_users: :environment do
+ ActionMailer::Base.delivery_method = :test
+
+ CSV.foreach('db/users.csv') do |row|
+ next if row[0] == 'encrypted_password'
+
+ create_user_from row
+ end
+ end
+
+
+ private
+
+ def users
+ # Skip some spambot users
+ Spree::User.all.reject { |u| u.email =~ /example.net/ }
+ end
+
+
+ def header
+ ["encrypted_password", "password_salt", "email", "remember_token", "persistence_token", "reset_password_token", "perishable_token", "sign_in_count", "failed_attempts", "last_request_at", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "login", "created_at", "updated_at", "authentication_token", "unlock_token", "locked_at", "remember_created_at", "reset_password_sent_at",
+
+ "role_name",
+
+ "ship_address_firstname", "ship_address_lastname", "ship_address_address1", "ship_address_address2", "ship_address_city", "ship_address_zipcode", "ship_address_phone", "ship_address_state", "ship_address_country", "ship_address_created_at", "ship_address_updated_at", "ship_address_company",
+
+ "bill_address_firstname", "bill_address_lastname", "bill_address_address1", "bill_address_address2", "bill_address_city", "bill_address_zipcode", "bill_address_phone", "bill_address_state", "bill_address_country", "bill_address_created_at", "bill_address_updated_at", "bill_address_company",]
+ end
+
+
+ def row(user)
+ sa = user.orders.last.andand.ship_address
+ ba = user.orders.last.andand.bill_address
+
+ [user.encrypted_password, user.password_salt, user.email, user.remember_token, user.persistence_token, user.reset_password_token, user.perishable_token, user.sign_in_count, user.failed_attempts, user.last_request_at, user.current_sign_in_at, user.last_sign_in_at, user.current_sign_in_ip, user.last_sign_in_ip, user.login, user.created_at, user.updated_at, user.authentication_token, user.unlock_token, user.locked_at, user.remember_created_at, user.reset_password_sent_at,
+
+ user.spree_roles.first.andand.name,
+
+ sa.andand.firstname, sa.andand.lastname, sa.andand.address1, sa.andand.address2, sa.andand.city, sa.andand.zipcode, sa.andand.phone, sa.andand.state, sa.andand.country, sa.andand.created_at, sa.andand.updated_at, sa.andand.company,
+
+ ba.andand.firstname, ba.andand.lastname, ba.andand.address1, ba.andand.address2, ba.andand.city, ba.andand.zipcode, ba.andand.phone, ba.andand.state, ba.andand.country, ba.andand.created_at, ba.andand.updated_at, ba.andand.company,]
+ end
+
+
+ def create_user_from(row)
+ user = Spree::User.create!({password: 'changeme123', password_confirmation: 'changeme123', email: row[2], remember_token: row[3], persistence_token: row[4], reset_password_token: row[5], perishable_token: row[6], sign_in_count: row[7], failed_attempts: row[8], last_request_at: row[9], current_sign_in_at: row[10], last_sign_in_at: row[11], current_sign_in_ip: row[12], last_sign_in_ip: row[13], login: row[14], created_at: row[15], updated_at: row[16], authentication_token: row[17], unlock_token: row[18], locked_at: row[19], remember_created_at: row[20], reset_password_sent_at: row[21]}, without_protection: true)
+
+ user.update_column :encrypted_password, row[0]
+ user.update_column :password_salt, row[1]
+
+ # Safer if we don't make new users into admins
+ #role = Spree::Role.find_by_name row[24]
+ #user.spree_roles << role if role
+
+ sa_state = Spree::State.find_by_name row[30]
+ sa_country = Spree::Country.find_by_name row[31]
+ sa = Spree::Address.create!({firstname: row[23], lastname: row[24], address1: row[25], address2: row[26], city: row[27], zipcode: row[28], phone: row[29], state: sa_state, country: sa_country, created_at: row[32], updated_at: row[33], company: row[34]}, without_protection: true)
+ user.update_column :ship_address_id, sa.id
+
+ ba_state = Spree::State.find_by_name row[42]
+ ba_country = Spree::Country.find_by_name row[43]
+ ba = Spree::Address.create!({firstname: row[35], lastname: row[36], address1: row[37], address2: row[38], city: row[39], zipcode: row[40], phone: row[41], state: ba_state, country: ba_country, created_at: row[44], updated_at: row[45], company: row[46]}, without_protection: true)
+ user.update_column :bill_address_id, ba.id
+
+ rescue ActiveRecord::RecordInvalid => e
+ puts "#{row[2]} - #{e.message}"
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
new file mode 100644
index 0000000000..4005d82a2a
--- /dev/null
+++ b/spec/controllers/groups_controller_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe GroupsController do
+ it "gets all visible groups" do
+ EnterpriseGroup.stub_chain :on_front_page, :by_position
+ EnterpriseGroup.should_receive :on_front_page
+ get :index
+ end
+end
diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb
index 88cde0e6d9..f0857f54bc 100644
--- a/spec/controllers/shop_controller_spec.rb
+++ b/spec/controllers/shop_controller_spec.rb
@@ -111,90 +111,6 @@ describe ShopController do
response.body.should be_empty
end
- # TODO: this should be a controller test baby
- pending "filtering products" do
- let(:distributor) { create(:distributor_enterprise) }
- let(:supplier) { create(:supplier_enterprise) }
- let(:oc1) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), orders_close_at: 2.days.from_now) }
- let(:p1) { create(:simple_product, on_demand: false) }
- let(:p2) { create(:simple_product, on_demand: true) }
- let(:p3) { create(:simple_product, on_demand: false) }
- let(:p4) { create(:simple_product, on_demand: false) }
- let(:p5) { create(:simple_product, on_demand: false) }
- let(:p6) { create(:simple_product, on_demand: false) }
- let(:p7) { create(:simple_product, on_demand: false) }
- let(:v1) { create(:variant, product: p4, unit_value: 2) }
- let(:v2) { create(:variant, product: p4, unit_value: 3, on_demand: false) }
- let(:v3) { create(:variant, product: p4, unit_value: 4, on_demand: true) }
- let(:v4) { create(:variant, product: p5) }
- let(:v5) { create(:variant, product: p5) }
- let(:v6) { create(:variant, product: p7) }
- let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) }
-
- before do
- p1.master.count_on_hand = 1
- p2.master.count_on_hand = 0
- p1.master.update_attribute(:count_on_hand, 1)
- p2.master.update_attribute(:count_on_hand, 0)
- p3.master.update_attribute(:count_on_hand, 0)
- p6.master.update_attribute(:count_on_hand, 1)
- p6.delete
- p7.master.update_attribute(:count_on_hand, 1)
- v1.update_attribute(:count_on_hand, 1)
- v2.update_attribute(:count_on_hand, 0)
- v3.update_attribute(:count_on_hand, 0)
- v4.update_attribute(:count_on_hand, 1)
- v5.update_attribute(:count_on_hand, 0)
- v6.update_attribute(:count_on_hand, 1)
- v6.update_attribute(:deleted_at, Time.now)
- exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id)
- exchange.update_attribute :pickup_time, "frogs"
- exchange.variants << p1.master
- exchange.variants << p2.master
- exchange.variants << p3.master
- exchange.variants << p6.master
- exchange.variants << v1
- exchange.variants << v2
- exchange.variants << v3
- # v4 is in stock but not in distribution
- # v5 is out of stock and in the distribution
- # Neither should display, nor should their product, p5
- exchange.variants << v5
- exchange.variants << v6
-
- controller.stub(:current_order).and_return order
- visit shop_path
- end
-
- it "filters products based on availability" do
- # It shows on hand products
- page.should have_content p1.name
- page.should have_content p4.name
-
- # It shows on demand products
- page.should have_content p2.name
-
- # It does not show products that are neither on hand or on demand
- page.should_not have_content p3.name
-
- # It shows on demand variants
- page.should have_content v3.options_text
-
- # It does not show variants that are neither on hand or on demand
- page.should_not have_content v2.options_text
-
- # It does not show products that have no available variants in this distribution
- page.should_not have_content p5.name
-
- # It does not show deleted products
- page.should_not have_content p6.name
-
- # It does not show deleted variants
- page.should_not have_content v6.name
- page.should_not have_content p7.name
- end
- end
-
context "RABL tests" do
render_views
before do
@@ -211,6 +127,7 @@ describe ShopController do
xhr :get, :products
response.body.should_not have_content product.name
end
+
it "strips html from description" do
product.update_attribute(:description, "turtles frogs")
xhr :get, :products
@@ -223,6 +140,14 @@ describe ShopController do
xhr :get, :products
response.body.should have_content "998.0"
end
+
+ it "includes the primary taxon" do
+ taxon = mock_model(Spree::Taxon, name: "fruitbat")
+ Spree::Product.any_instance.stub(:primary_taxon).and_return taxon
+ taxon.stub_chain(:icon, :url).and_return ""
+ xhr :get, :products
+ response.body.should have_content "fruitbat"
+ end
end
end
end
diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb
index a9118355d6..e6c9b91894 100644
--- a/spec/controllers/spree/orders_controller_spec.rb
+++ b/spec/controllers/spree/orders_controller_spec.rb
@@ -2,12 +2,24 @@ require 'spec_helper'
describe Spree::OrdersController do
let(:distributor) { double(:distributor) }
+ let(:order) { create(:order) }
+ let(:order_cycle) { create(:simple_order_cycle) }
it "redirects home when no distributor is selected" do
spree_get :edit
response.should redirect_to root_path
end
+ it "redirects to shop when order is empty" do
+ controller.stub(:current_distributor).and_return(distributor)
+ controller.stub(:current_order_cycle).and_return(order_cycle)
+ controller.stub(:current_order).and_return order
+ order.stub_chain(:line_items, :empty?).and_return true
+ session[:access_token] = order.token
+ spree_get :edit
+ response.should redirect_to shop_path
+ end
+
it "redirects to the shop when no order cycle is selected" do
controller.stub(:current_distributor).and_return(distributor)
spree_get :edit
diff --git a/spec/controllers/spree/store_controller_spec.rb b/spec/controllers/spree/store_controller_spec.rb
new file mode 100644
index 0000000000..e1b94de185
--- /dev/null
+++ b/spec/controllers/spree/store_controller_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Spree::StoreController do
+ controller(Spree::StoreController) do
+ before_filter :unauthorized
+ def index
+ render text: ""
+ end
+ end
+ it "redirects to home when unauthorized" do
+ get :index
+ response.should render_template("shared/unauthorized", layout: 'darkswarm')
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index 710ba1c8ba..6da7824d47 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -96,8 +96,12 @@ FactoryGirl.define do
is_distributor true
end
+ factory :enterprise_relationship do
+ end
+
factory :enterprise_group, :class => EnterpriseGroup do
name 'Enterprise group'
+ description 'this is a group'
on_front_page false
end
@@ -186,6 +190,10 @@ FactoryGirl.modify do
distributors { [Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise)] }
end
+ factory :option_type do
+ # Prevent inconsistent ordering in specs when all option types have the same (0) position
+ sequence(:position)
+ end
end
diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb
index 79981bbd16..963cd04f8d 100644
--- a/spec/features/admin/bulk_order_management_spec.rb
+++ b/spec/features/admin/bulk_order_management_spec.rb
@@ -206,7 +206,7 @@ feature %q{
page.should have_selector "tr#li_#{li2.id}", visible: true
select2_select s1.name, from: "supplier_filter"
page.should have_selector "tr#li_#{li1.id}", visible: true
- page.should_not have_selector "tr#li_#{li2.id}", visible: true
+ page.should_not have_selector "tr#li_#{li2.id}"
end
it "displays all line items when 'All' is selected from supplier filter" do
diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb
index 7174da9758..d89280df80 100644
--- a/spec/features/admin/bulk_product_update_spec.rb
+++ b/spec/features/admin/bulk_product_update_spec.rb
@@ -236,9 +236,7 @@ feature %q{
fill_in 'product_name', :with => 'Big Bag Of Apples'
select(s.name, :from => 'product_supplier_id')
- choose('product_group_buy_0')
fill_in 'product_price', :with => '10.00'
- fill_in 'product_available_on', :with => Date.today.strftime("%Y/%m/%d")
click_button 'Create'
URI.parse(current_url).path.should == '/admin/products/bulk_edit'
diff --git a/spec/features/admin/enterprise_groups_spec.rb b/spec/features/admin/enterprise_groups_spec.rb
index 012179e75a..24e89e9be7 100644
--- a/spec/features/admin/enterprise_groups_spec.rb
+++ b/spec/features/admin/enterprise_groups_spec.rb
@@ -33,6 +33,7 @@ feature %q{
click_link 'New Enterprise Group'
fill_in 'enterprise_group_name', with: 'EGEGEG'
+ fill_in 'enterprise_group_description', with: 'This is a description'
check 'enterprise_group_on_front_page'
select e1.name, from: 'enterprise_group_enterprise_ids'
select e2.name, from: 'enterprise_group_enterprise_ids'
@@ -42,6 +43,7 @@ feature %q{
eg = EnterpriseGroup.last
eg.name.should == 'EGEGEG'
+ eg.description.should == 'This is a description'
eg.on_front_page.should be_true
eg.enterprises.sort.should == [e1, e2].sort
end
@@ -62,6 +64,7 @@ feature %q{
fill_in 'enterprise_group_name', with: 'xyzzy'
uncheck 'enterprise_group_on_front_page'
unselect e1.name, from: 'enterprise_group_enterprise_ids'
+
select e2.name, from: 'enterprise_group_enterprise_ids'
click_button 'Update'
@@ -99,7 +102,6 @@ feature %q{
EnterpriseGroup.all.should_not include eg
end
-
context "as an enterprise user" do
xit "should show me only enterprises I manage when creating a new enterprise group"
end
diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb
new file mode 100644
index 0000000000..1443cd4c34
--- /dev/null
+++ b/spec/features/admin/enterprise_relationships_spec.rb
@@ -0,0 +1,114 @@
+require 'spec_helper'
+
+feature %q{
+ As an Administrator
+ I want to manage relationships between enterprises
+}, js: true do
+ include AuthenticationWorkflow
+ include WebHelper
+
+
+ context "as a site administrator" do
+ before { login_to_admin_section }
+
+ scenario "listing relationships" do
+ # Given some enterprises with relationships
+ e1, e2, e3, e4 = create(:enterprise), create(:enterprise), create(:enterprise), create(:enterprise)
+ create(:enterprise_relationship, parent: e1, child: e2)
+ create(:enterprise_relationship, parent: e3, child: e4)
+
+ # When I go to the relationships page
+ click_link 'Enterprises'
+ click_link 'Relationships'
+
+ # Then I should see the relationships
+ within('table#enterprise-relationships') do
+ page.should have_relationship e1, e2
+ page.should have_relationship e3, e4
+ end
+ end
+
+
+ scenario "creating a relationship" do
+ e1 = create(:enterprise, name: 'One')
+ e2 = create(:enterprise, name: 'Two')
+
+ visit admin_enterprise_relationships_path
+ select 'One', from: 'enterprise_relationship_parent_id'
+ select 'Two', from: 'enterprise_relationship_child_id'
+ click_button 'Create'
+
+ page.should have_relationship e1, e2
+ EnterpriseRelationship.where(parent_id: e1, child_id: e2).should be_present
+ end
+
+
+ scenario "attempting to create a relationship with invalid data" do
+ e1 = create(:enterprise, name: 'One')
+ e2 = create(:enterprise, name: 'Two')
+ create(:enterprise_relationship, parent: e1, child: e2)
+
+ expect do
+ # When I attempt to create a duplicate relationship
+ visit admin_enterprise_relationships_path
+ select 'One', from: 'enterprise_relationship_parent_id'
+ select 'Two', from: 'enterprise_relationship_child_id'
+ click_button 'Create'
+
+ # Then I should see an error message
+ page.should have_content "That relationship is already established."
+ end.to change(EnterpriseRelationship, :count).by(0)
+ end
+
+
+ scenario "deleting a relationship" do
+ e1 = create(:enterprise, name: 'One')
+ e2 = create(:enterprise, name: 'Two')
+ er = create(:enterprise_relationship, parent: e1, child: e2)
+
+ visit admin_enterprise_relationships_path
+ page.should have_relationship e1, e2
+
+ first("a.delete-enterprise-relationship").click
+
+ page.should_not have_relationship e1, e2
+ EnterpriseRelationship.where(id: er.id).should be_empty
+ end
+ end
+
+
+ context "as an enterprise user" do
+ let!(:d1) { create(:distributor_enterprise) }
+ let!(:d2) { create(:distributor_enterprise) }
+ let!(:d3) { create(:distributor_enterprise) }
+ let(:enterprise_user) { create_enterprise_user([d1]) }
+
+ let!(:er1) { create(:enterprise_relationship, parent: d1, child: d2) }
+ let!(:er2) { create(:enterprise_relationship, parent: d2, child: d1) }
+ let!(:er3) { create(:enterprise_relationship, parent: d2, child: d3) }
+
+ before { login_to_admin_as enterprise_user }
+
+ scenario "enterprise user can only see relationships involving their enterprises" do
+ visit admin_enterprise_relationships_path
+
+ page.should have_relationship d1, d2
+ page.should have_relationship d2, d1
+ page.should_not have_relationship d2, d3
+ end
+
+
+ scenario "enterprise user can only add their own enterprises as parent" do
+ visit admin_enterprise_relationships_path
+ page.should have_select 'enterprise_relationship_parent_id', options: ['', d1.name]
+ page.should have_select 'enterprise_relationship_child_id', options: ['', d1.name, d2.name, d3.name]
+ end
+ end
+
+
+ private
+
+ def have_relationship(parent, child)
+ have_table_row [parent.name, 'permits', child.name, '']
+ end
+end
diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb
index f6d5e32fec..765a6251bf 100644
--- a/spec/features/admin/enterprises_spec.rb
+++ b/spec/features/admin/enterprises_spec.rb
@@ -76,6 +76,8 @@ feature %q{
fill_in 'enterprise_email', :with => 'info@eaterprises.com.au'
fill_in 'enterprise_website', :with => 'http://eaterprises.com.au'
fill_in 'enterprise_twitter', :with => '@eaterprises'
+ fill_in 'enterprise_facebook', :with => 'facebook.com/eaterprises'
+ fill_in 'enterprise_instagram', :with => 'eaterprises'
fill_in 'enterprise_abn', :with => '09812309823'
fill_in 'enterprise_acn', :with => ''
diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb
index 6cb3a501a4..2423a6dc05 100644
--- a/spec/features/admin/order_cycles_spec.rb
+++ b/spec/features/admin/order_cycles_spec.rb
@@ -514,6 +514,15 @@ feature %q{
# I should not see exchanges for supplier2 or distributor2
page.all('tr.supplier').count.should == 1
page.all('tr.distributor').count.should == 1
+
+ # When I save, then those exchanges should remain
+ click_button 'Update'
+ page.should have_content "Your order cycle has been updated."
+
+ oc.reload
+ oc.suppliers.sort.should == [supplier1, supplier2]
+ oc.coordinator.should == supplier1
+ oc.distributors.sort.should == [distributor1, distributor2]
end
scenario "cloning an order cycle" do
diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb
index b9f9d7a206..63a73ccbff 100644
--- a/spec/features/admin/products_spec.rb
+++ b/spec/features/admin/products_spec.rb
@@ -32,7 +32,7 @@ feature %q{
product.group_buy.should be_false
# Distributors
- within('#sidebar') { click_link 'Product Distributions' }
+ visit spree.product_distributions_admin_product_path(product)
check @distributors[0].name
select @enterprise_fees[0].name, :from => 'product_product_distributions_attributes_0_enterprise_fee_id'
@@ -46,23 +46,20 @@ feature %q{
product.product_distributions.map { |pd| pd.enterprise_fee }.sort.should == [@enterprise_fees[0], @enterprise_fees[2]].sort
end
+ scenario "making a product into a group buy product" do
+ product = create(:simple_product, name: 'group buy product')
- scenario "creating a group buy product" do
login_to_admin_section
- click_link 'Products'
- click_link 'New Product'
+ visit spree.edit_admin_product_path(product)
- fill_in 'product_name', :with => 'A new product !!!'
- fill_in 'product_price', :with => '19.99'
- select 'New supplier', :from => 'product_supplier_id'
choose 'product_group_buy_1'
fill_in 'Group buy unit size', :with => '10'
- click_button 'Create'
+ click_button 'Update'
- flash_message.should == 'Product "A new product !!!" has been successfully created!'
- product = Spree::Product.find_by_name('A new product !!!')
+ flash_message.should == 'Product "group buy product" has been successfully updated!'
+ product.reload
product.group_buy.should be_true
product.group_buy_unit_size.should == 10.0
end
diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb
index 77b60290d1..76e4a20877 100644
--- a/spec/features/consumer/authentication_spec.rb
+++ b/spec/features/consumer/authentication_spec.rb
@@ -5,7 +5,27 @@ feature "Authentication", js: true do
describe "login" do
let(:user) { create(:user, password: "password", password_confirmation: "password") }
- describe "newskool" do
+ describe "With redirects" do
+ scenario "logging in with a redirect set" do
+ visit groups_path(anchor: "login?after_login=#{producers_path}")
+ fill_in "Email", with: user.email
+ fill_in "Password", with: user.password
+ click_login_button
+ page.should have_content "Select a producer from the list below"
+ current_path.should == producers_path
+ end
+
+ scenario "logging into admin redirects home, then back to admin" do
+ visit spree.admin_path
+ fill_in "Email", with: user.email
+ fill_in "Password", with: user.password
+ click_login_button
+ page.should have_content "Dashboard"
+ current_path.should == spree.admin_path
+ end
+ end
+
+ describe "Loggin in from the home page" do
before do
visit root_path
end
diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb
index 19fcb5a44b..9946be2f5b 100644
--- a/spec/features/consumer/shopping/shopping_spec.rb
+++ b/spec/features/consumer/shopping/shopping_spec.rb
@@ -90,10 +90,6 @@ feature "As a consumer I want to shop with a distributor", js: true do
it "should not show quantity field for product with variants" do
visit shop_path
page.should_not have_selector("#variants_#{product.master.id}", visible: true)
-
- #it "expands variants" do
- find(".collapse").trigger "click"
- page.should_not have_text variant1.options_text
end
it "uses the adjusted price" do
@@ -104,15 +100,16 @@ feature "As a consumer I want to shop with a distributor", js: true do
visit shop_path
# Page should not have product.price (with or without fee)
- page.should_not have_selector 'tr.product > td', text: "from $10.00"
- page.should_not have_selector 'tr.product > td', text: "from $33.00"
+ page.should_not have_price "from $10.00"
+ page.should_not have_price "from $33.00"
# Page should have variant prices (with fee)
- page.should have_selector 'tr.variant > td.price', text: "$43.00"
- page.should have_selector 'tr.variant > td.price', text: "$53.00"
+ page.should have_price "$43.00"
+ page.should have_price "$53.00"
# Product price should be listed as the lesser of these
- page.should have_selector 'tr.product > td', text: "from $43.00"
+ #page.should have_selector 'tr.product > td', text: "from $43.00"
+ page.should have_price "from $43.00"
end
end
@@ -131,7 +128,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
it "should save group buy data to ze cart" do
fill_in "variants[#{product.master.id}]", with: 5
fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 9
- first("form.custom > input.button.right").click
+ add_to_cart
page.should have_content product.name
li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last
li.max_quantity.should == 9
@@ -142,7 +139,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
pending "adding a product with a max quantity less than quantity results in max_quantity==quantity" do
fill_in "variants[#{product.master.id}]", with: 5
fill_in "variant_attributes[#{product.master.id}][max_quantity]", with: 1
- first("form.custom > input.button.right").click
+ add_to_cart
page.should have_content product.name
li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last
li.max_quantity.should == 5
@@ -161,7 +158,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
it "should save group buy data to ze cart" do
fill_in "variants[#{variant.id}]", with: 6
fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7
- first("form.custom > input.button.right").click
+ add_to_cart
page.should have_content product.name
li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last
li.max_quantity.should == 7
@@ -181,7 +178,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
end
it "should let us add products to our cart" do
fill_in "variants[#{variant.id}]", with: "1"
- first("form.custom > input.button.right").click
+ add_to_cart
current_path.should == "/cart"
page.should have_content product.name
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
new file mode 100644
index 0000000000..08a4335bae
--- /dev/null
+++ b/spec/helpers/groups_helper_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+# Specs in this file have access to a helper object that includes
+# the GroupsHelper. For example:
+#
+# describe GroupsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+describe GroupsHelper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/helpers/shared_helper_spec.rb b/spec/helpers/shared_helper_spec.rb
index d28a6eb7b3..92c688d1ae 100644
--- a/spec/helpers/shared_helper_spec.rb
+++ b/spec/helpers/shared_helper_spec.rb
@@ -23,37 +23,4 @@ describe SharedHelper do
helper.stub(:current_order) { order }
helper.distributor_link_class(d1).should =~ /empties-cart/
end
-
- describe "finding current producers" do
- it "finds producers for the current distribution" do
- s = create(:supplier_enterprise)
- d = create(:distributor_enterprise)
- p = create(:simple_product)
- oc = create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master])
-
- helper.stub(:current_order_cycle) { oc }
- helper.stub(:current_distributor) { d }
-
- helper.current_producers.should == [s]
- end
-
- it "returns [] when no order cycle set" do
- d = double(:distributor)
-
- helper.stub(:current_order_cycle) { nil }
- helper.stub(:current_distributor) { d }
-
- helper.current_producers.should == []
- end
-
- it "returns [] when no distributor set" do
- oc = double(:order_cycle)
-
- helper.stub(:current_order_cycle) { oc }
- helper.stub(:current_distributor) { nil }
-
- helper.current_producers.should == []
-
- end
- end
end
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
index 9e821754de..df629c1c81 100644
--- a/spec/javascripts/application_spec.js
+++ b/spec/javascripts/application_spec.js
@@ -1,6 +1,7 @@
//= require angular
//= require angular-resource
//= require angular-animate
+//= require angular-sanitize
//= require angular-mocks
//= require angular-cookies
//= require angular-backstretch.js
diff --git a/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee
index a2d14f5811..cb77cb1703 100644
--- a/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee
+++ b/spec/javascripts/unit/darkswarm/controllers/hub_node_controller_spec.js.coffee
@@ -19,14 +19,3 @@ describe "HubNodeCtrl", ->
expect(scope.current()).toEqual false
scope.hub = {id: 99}
expect(scope.current()).toEqual true
-
- it "knows whether selecting this hub will empty the cart", ->
- CurrentHub.id = undefined
- expect(scope.emptiesCart()).toEqual false
-
- CurrentHub.id = 99
- scope.hub.id = 99
- expect(scope.emptiesCart()).toEqual false
-
- scope.hub.id = 1
- expect(scope.emptiesCart()).toEqual true
diff --git a/spec/javascripts/unit/darkswarm/controllers/products/product_node_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/products/product_node_controller_spec.js.coffee
new file mode 100644
index 0000000000..4b9e404b9e
--- /dev/null
+++ b/spec/javascripts/unit/darkswarm/controllers/products/product_node_controller_spec.js.coffee
@@ -0,0 +1,24 @@
+describe "ProductNodeCtrl", ->
+ ctrl = null
+ scope = null
+ product =
+ id: 99
+ price: 10.00
+ variants: []
+
+ beforeEach ->
+ module('Darkswarm')
+ inject ($controller) ->
+ scope =
+ product: product
+ ctrl = $controller 'ProductNodeCtrl', {$scope: scope}
+
+ describe "determining the price to display for a product", ->
+ it "displays the product price when the product does not have variants", ->
+ expect(scope.price()).toEqual 10.00
+
+ it "displays the minimum variant price when the product has variants", ->
+ scope.product =
+ price: 11
+ variants: [{price: 22}, {price: 33}]
+ expect(scope.price()).toEqual 22
diff --git a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee
index 269c8385b8..685d495039 100644
--- a/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee
+++ b/spec/javascripts/unit/darkswarm/controllers/products_controller_spec.js.coffee
@@ -1,47 +1,21 @@
-describe 'All controllers', ->
- describe 'ProductsCtrl', ->
- ctrl = null
- scope = null
- event = null
- Product = null
+describe 'ProductsCtrl', ->
+ ctrl = null
+ scope = null
+ event = null
+ Product = null
- beforeEach ->
- module('Darkswarm')
- Product =
- all: ->
- update: ->
- data: "testy mctest"
- OrderCycle =
- order_cycle: {}
-
- inject ($controller) ->
- scope = {}
- ctrl = $controller 'ProductsCtrl', {$scope: scope, Product: Product, OrderCycle: OrderCycle}
-
- it 'fetches products from Product', ->
- expect(scope.data).toEqual 'testy mctest'
-
- describe "determining the price to display for a product", ->
- it "displays the product price when the product does not have variants", ->
- product = {variants: [], price: 12.34}
- expect(scope.productPrice(product)).toEqual 12.34
-
- it "displays the minimum variant price when the product has variants", ->
- product =
- price: 11
- variants: [{price: 22}, {price: 33}]
- expect(scope.productPrice(product)).toEqual 22
-
- describe 'OrderCycleCtrl', ->
- ctrl = null
- scope = null
- event = null
- product_ctrl = null
- OrderCycle = null
-
- beforeEach ->
- module 'Darkswarm'
+ beforeEach ->
+ module('Darkswarm')
+ Product =
+ all: ->
+ update: ->
+ data: "testy mctest"
+ OrderCycle =
+ order_cycle: {}
+
+ inject ($controller) ->
scope = {}
- inject ($controller) ->
- scope = {}
- ctrl = $controller 'OrderCycleCtrl', {$scope: scope}
+ ctrl = $controller 'ProductsCtrl', {$scope: scope, Product: Product, OrderCycle: OrderCycle}
+
+ it 'fetches products from Product', ->
+ expect(scope.data).toEqual 'testy mctest'
diff --git a/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee
new file mode 100644
index 0000000000..f34df5cc71
--- /dev/null
+++ b/spec/javascripts/unit/darkswarm/filters/strip_url_spec.js.coffee
@@ -0,0 +1,16 @@
+describe 'filtering urls', ->
+ filter = null
+
+ beforeEach ->
+ module 'Darkswarm'
+ inject ($filter) ->
+ filter = $filter('stripUrl')
+
+ it "removes http and www", ->
+ expect(filter("http://www.footle.com")).toEqual "footle.com"
+
+ it "removes https and www", ->
+ expect(filter("https://www.footle.com")).toEqual "footle.com"
+
+ it "removes just www", ->
+ expect(filter("www.footle.com")).toEqual "footle.com"
diff --git a/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee
index f7a65867e8..93dd7de79c 100644
--- a/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee
+++ b/spec/javascripts/unit/darkswarm/services/order_cycle_spec.js.coffee
@@ -21,14 +21,14 @@ describe 'OrderCycle service', ->
$httpBackend.expectPOST("/shop/order_cycle", {"order_cycle_id" : 10}).respond(200)
spyOn(mockProduct, "update")
OrderCycle.order_cycle.order_cycle_id = 10
- OrderCycle.push_order_cycle()
+ OrderCycle.push_order_cycle mockProduct.update
$httpBackend.flush()
expect(mockProduct.update).toHaveBeenCalled()
it "updates the orders_close_at attr after update", ->
datestring = "2013-12-20T00:00:00+11:00"
$httpBackend.expectPOST("/shop/order_cycle").respond({orders_close_at: datestring})
- OrderCycle.push_order_cycle()
+ OrderCycle.push_order_cycle mockProduct.update
$httpBackend.flush()
expect(OrderCycle.order_cycle.orders_close_at).toEqual(datestring)
diff --git a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee
index 0462e92952..4f6c711f99 100644
--- a/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee
+++ b/spec/javascripts/unit/darkswarm/services/order_spec.js.coffee
@@ -26,6 +26,7 @@ describe 'Order service', ->
}
angular.module('Darkswarm').value('order', orderData)
module 'Darkswarm'
+
inject ($injector, _$httpBackend_)->
$httpBackend = _$httpBackend_
Order = $injector.get("Order")
@@ -75,7 +76,6 @@ describe 'Order service', ->
$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)
diff --git a/spec/javascripts/unit/darkswarm/product_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee
similarity index 86%
rename from spec/javascripts/unit/darkswarm/product_spec.js.coffee
rename to spec/javascripts/unit/darkswarm/services/product_spec.js.coffee
index 7108d819af..260074a981 100644
--- a/spec/javascripts/unit/darkswarm/product_spec.js.coffee
+++ b/spec/javascripts/unit/darkswarm/services/product_spec.js.coffee
@@ -10,5 +10,5 @@ describe 'Product service', ->
it "Fetches products from the backend on init", ->
$httpBackend.expectGET("/shop/products").respond([{test : "cats"}])
- products = Product.all()
$httpBackend.flush()
+ expect(Product.data.products[0].test).toEqual "cats"
diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb
index 81cd925569..c63d1eac25 100644
--- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb
+++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb
@@ -11,7 +11,7 @@ module OpenFoodNetwork
oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [incoming_exchange], :outgoing_exchanges => [])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.should_receive(:exchange_variant_ids).with(incoming_exchange).and_return([1, 3])
applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(false)
@@ -29,7 +29,7 @@ module OpenFoodNetwork
oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [], :outgoing_exchanges => [outgoing_exchange])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.should_receive(:exchange_variant_ids).with(outgoing_exchange).and_return([1, 3])
applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(false)
@@ -51,7 +51,7 @@ module OpenFoodNetwork
:incoming_exchanges => [incoming_exchange],
:outgoing_exchanges => [])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.should_receive(:exchange_variant_ids).with(incoming_exchange).and_return([1, 3])
applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(true)
@@ -73,7 +73,7 @@ module OpenFoodNetwork
:incoming_exchanges => [],
:outgoing_exchanges => [outgoing_exchange])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.should_receive(:exchange_variant_ids).with(outgoing_exchange).and_return([1, 3])
applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(true)
@@ -95,7 +95,7 @@ module OpenFoodNetwork
:incoming_exchanges => [],
:outgoing_exchanges => [])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.should_receive(:destroy_untouched_exchanges)
@@ -108,20 +108,61 @@ module OpenFoodNetwork
e2 = double(:exchange2, id: 1, foo: 2)
oc = double(:order_cycle, :exchanges => [e1])
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.instance_eval do
@touched_exchanges = [e2]
end
applicator.send(:untouched_exchanges).should == []
end
+
+ it "does not destroy exchanges involving enterprises it does not have permission to touch" do
+ applicator = OrderCycleFormApplicator.new(nil, [])
+ exchanges = double(:exchanges)
+ permitted_exchanges = [double(:exchange), double(:exchange)]
+
+ applicator.should_receive(:with_permission).with(exchanges) { permitted_exchanges }
+ applicator.stub(:untouched_exchanges) { exchanges }
+ permitted_exchanges.each { |ex| ex.should_receive(:destroy) }
+
+ applicator.send(:destroy_untouched_exchanges)
+ end
end
it "converts exchange variant ids hash to an array of ids" do
- applicator = OrderCycleFormApplicator.new(nil)
+ applicator = OrderCycleFormApplicator.new(nil, [])
applicator.send(:exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}).should == [1, 3]
end
+
+ describe "filtering exchanges for permission" do
+ describe "checking permission on a single exchange" do
+ it "returns true when it has permission" do
+ e = double(:enterprise)
+ ex = double(:exchange, participant: e)
+
+ applicator = OrderCycleFormApplicator.new(nil, [e])
+ applicator.send(:permission_for, ex).should be_true
+ end
+
+ it "returns false otherwise" do
+ e = double(:enterprise)
+ ex = double(:exchange, participant: e)
+
+ applicator = OrderCycleFormApplicator.new(nil, [])
+ applicator.send(:permission_for, ex).should be_false
+ end
+ end
+
+ describe "filtering many exchanges" do
+ it "returns exchanges involving enterprises we have permission to touch" do
+ ex1, ex2 = double(:exchange), double(:exchange)
+ applicator = OrderCycleFormApplicator.new(nil, [])
+ applicator.stub(:permission_for).and_return(true, false)
+ applicator.send(:with_permission, [ex1, ex2]).should == [ex1]
+ end
+ end
+ end
end
context "integration specs" do
@@ -132,7 +173,7 @@ module OpenFoodNetwork
it "checks whether exchanges exist" do
oc = FactoryGirl.create(:simple_order_cycle)
exchange = FactoryGirl.create(:exchange, order_cycle: oc)
- applicator = OrderCycleFormApplicator.new(oc)
+ applicator = OrderCycleFormApplicator.new(oc, [])
applicator.send(:exchange_exists?, exchange.sender_id, exchange.receiver_id, exchange.incoming).should be_true
applicator.send(:exchange_exists?, exchange.sender_id, exchange.receiver_id, !exchange.incoming).should be_false
@@ -143,10 +184,10 @@ module OpenFoodNetwork
end
it "adds exchanges" do
- oc = FactoryGirl.create(:simple_order_cycle)
- applicator = OrderCycleFormApplicator.new(oc)
sender = FactoryGirl.create(:enterprise)
receiver = FactoryGirl.create(:enterprise)
+ oc = FactoryGirl.create(:simple_order_cycle)
+ applicator = OrderCycleFormApplicator.new(oc, [sender, receiver])
incoming = true
variant1 = FactoryGirl.create(:variant)
variant2 = FactoryGirl.create(:variant)
@@ -167,10 +208,11 @@ module OpenFoodNetwork
end
it "updates exchanges" do
- oc = FactoryGirl.create(:simple_order_cycle)
- applicator = OrderCycleFormApplicator.new(oc)
sender = FactoryGirl.create(:enterprise)
receiver = FactoryGirl.create(:enterprise)
+ oc = FactoryGirl.create(:simple_order_cycle)
+ applicator = OrderCycleFormApplicator.new(oc, [sender, receiver])
+
incoming = true
variant1 = FactoryGirl.create(:variant)
variant2 = FactoryGirl.create(:variant)
@@ -189,6 +231,34 @@ module OpenFoodNetwork
exchange.enterprise_fees.sort.should == [enterprise_fee2, enterprise_fee3]
applicator.send(:touched_exchanges).should == [exchange]
end
+
+ it "does not add exchanges it is not permitted to touch" do
+ sender = FactoryGirl.create(:enterprise)
+ receiver = FactoryGirl.create(:enterprise)
+ oc = FactoryGirl.create(:simple_order_cycle)
+ applicator = OrderCycleFormApplicator.new(oc, [])
+ incoming = true
+
+ expect do
+ applicator.send(:touched_exchanges=, [])
+ applicator.send(:add_exchange, sender.id, receiver.id, incoming)
+ end.to change(Exchange, :count).by(0)
+ end
+
+ it "does not update exchanges it is not permitted to touch" do
+ sender = FactoryGirl.create(:enterprise)
+ receiver = FactoryGirl.create(:enterprise)
+ oc = FactoryGirl.create(:simple_order_cycle)
+ applicator = OrderCycleFormApplicator.new(oc, [])
+ incoming = true
+ exchange = FactoryGirl.create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming)
+ variant1 = FactoryGirl.create(:variant)
+
+ applicator.send(:touched_exchanges=, [])
+ applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id]})
+
+ exchange.variants.should_not == [variant1]
+ end
end
end
end
diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb
index 167a8c2f7b..3e38882064 100644
--- a/spec/models/enterprise_group_spec.rb
+++ b/spec/models/enterprise_group_spec.rb
@@ -11,6 +11,13 @@ describe EnterpriseGroup do
e = build(:enterprise_group, name: '')
e.should_not be_valid
end
+
+ it "requires a description" do
+ e = build(:enterprise_group, description: '')
+ end
+
+ it { should have_attached_file :promo_image }
+ it { should have_attached_file :logo }
end
describe "relations" do
diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb
new file mode 100644
index 0000000000..b715674594
--- /dev/null
+++ b/spec/models/enterprise_relationship_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe EnterpriseRelationship do
+ describe "scopes" do
+ let(:e1) { create(:enterprise, name: 'A') }
+ let(:e2) { create(:enterprise, name: 'B') }
+ let(:e3) { create(:enterprise, name: 'C') }
+
+ it "sorts by parent, child enterprise name" do
+ er1 = create(:enterprise_relationship, parent: e1, child: e3)
+ er2 = create(:enterprise_relationship, parent: e2, child: e1)
+ er3 = create(:enterprise_relationship, parent: e1, child: e2)
+
+ EnterpriseRelationship.by_name.should == [er3, er1, er2]
+ end
+
+ describe "finding relationships involving some enterprises" do
+ let!(:er) { create(:enterprise_relationship, parent: e1, child: e2) }
+
+ it "returns relationships where an enterprise is the parent" do
+ EnterpriseRelationship.involving_enterprises([e1]).should == [er]
+ end
+
+ it "returns relationships where an enterprise is the child" do
+ EnterpriseRelationship.involving_enterprises([e2]).should == [er]
+ end
+
+ it "does not return other relationships" do
+ EnterpriseRelationship.involving_enterprises([e3]).should == []
+ end
+ end
+ end
+end
diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb
index 6cf933fdd6..d93bc78dd6 100644
--- a/spec/models/enterprise_spec.rb
+++ b/spec/models/enterprise_spec.rb
@@ -31,10 +31,10 @@ describe Enterprise do
let(:e) { create(:distributor_enterprise) }
let(:p) { create(:supplier_enterprise) }
let(:c) { create(:distributor_enterprise) }
- before do
- EnterpriseRelationship.create! parent_id: p.id, child_id: e.id
- EnterpriseRelationship.create! parent_id: e.id, child_id: c.id
- end
+
+ let!(:er1) { create(:enterprise_relationship, parent_id: p.id, child_id: e.id) }
+ let!(:er2) { create(:enterprise_relationship, parent_id: e.id, child_id: c.id) }
+
it "finds relatives" do
e.relatives.sort.should == [p, c].sort
end
diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb
index ed6cdbfc93..0dfceadbad 100644
--- a/spec/models/exchange_spec.rb
+++ b/spec/models/exchange_spec.rb
@@ -48,7 +48,7 @@ describe Exchange do
e.enterprise_fees.count.should == 1
end
- describe "reporting whether it is an incoming exchange" do
+ describe "exchange directionality" do
let(:supplier) { create(:supplier_enterprise) }
let(:coordinator) { create(:distributor_enterprise) }
let(:distributor) { create(:distributor_enterprise) }
@@ -56,12 +56,24 @@ describe Exchange do
let(:incoming_exchange) { oc.exchanges.create! sender: supplier, receiver: coordinator, incoming: true }
let(:outgoing_exchange) { oc.exchanges.create! sender: coordinator, receiver: distributor, incoming: false }
- it "returns true for incoming exchanges" do
- incoming_exchange.should be_incoming
+ describe "reporting whether it is an incoming exchange" do
+ it "returns true for incoming exchanges" do
+ incoming_exchange.should be_incoming
+ end
+
+ it "returns false for outgoing exchanges" do
+ outgoing_exchange.should_not be_incoming
+ end
end
- it "returns false for outgoing exchanges" do
- outgoing_exchange.should_not be_incoming
+ describe "finding the exchange participant (the enterprise other than the coordinator)" do
+ it "returns the sender for incoming exchanges" do
+ incoming_exchange.participant.should == supplier
+ end
+
+ it "returns the receiver for outgoing exchanges" do
+ outgoing_exchange.participant.should == distributor
+ end
end
end
diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb
index fc13925bae..111c8e5196 100644
--- a/spec/models/spree/ability_spec.rb
+++ b/spec/models/spree/ability_spec.rb
@@ -17,6 +17,9 @@ module Spree
let(:p1) { create(:product, supplier: s1, distributors:[d1, d2]) }
let(:p2) { create(:product, supplier: s2, distributors:[d1, d2]) }
+ let(:er1) { create(:enterprise_relationship, parent: s1, child: d1) }
+ let(:er2) { create(:enterprise_relationship, parent: d1, child: s1) }
+
subject { user }
let(:user) { nil }
@@ -72,6 +75,18 @@ module Spree
should have_ability([:admin, :index, :read, :create, :edit], for: Spree::Classification)
end
+ it "should be able to read and create enterprise relationships" do
+ should have_ability([:admin, :index, :create], for: EnterpriseRelationship)
+ end
+
+ it "should be able to destroy enterprise relationships for its enterprises" do
+ should have_ability(:destroy, for: er1)
+ end
+
+ it "should not be able to destroy enterprise relationships for other enterprises" do
+ should_not have_ability(:destroy, for: er2)
+ end
+
end
context "when is a distributor enterprise user" do
@@ -146,6 +161,18 @@ module Spree
it "should be able to read/write ShippingMethods" do
should have_ability([:admin, :index, :create, :update, :destroy], for: Spree::ShippingMethod)
end
+
+ it "should be able to read and create enterprise relationships" do
+ should have_ability([:admin, :index, :create], for: EnterpriseRelationship)
+ end
+
+ it "should be able to destroy enterprise relationships for its enterprises" do
+ should have_ability(:destroy, for: er2)
+ end
+
+ it "should not be able to destroy enterprise relationships for other enterprises" do
+ should_not have_ability(:destroy, for: er1)
+ end
end
context 'Order Cycle co-ordinator' do
diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb
index aa37070598..a5333b6d11 100644
--- a/spec/models/spree/product_spec.rb
+++ b/spec/models/spree/product_spec.rb
@@ -561,5 +561,15 @@ module Spree
end
end
end
+
+ describe "Taxons" do
+ let(:taxon1) { create(:taxon) }
+ let(:taxon2) { create(:taxon) }
+ let(:product) { create(:simple_product, taxons: [taxon1, taxon2]) }
+
+ it "returns the first taxon as the primary taxon" do
+ product.primary_taxon.should == taxon1
+ end
+ end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 49a7f39250..4f96a09070 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -71,26 +71,20 @@ RSpec.configure do |config|
# rspec-rails.
config.infer_base_class_for_anonymous_controllers = false
- # ## Filters
- #
+ # Filters
config.filter_run_excluding :skip => true, :future => true, :to_figure_out => true
- config.before(:each) do
- Spree::Address.any_instance.stub(:geocode).and_return([1,1])
+ # DatabaseCleaner
+ config.before(:suite) { DatabaseCleaner.clean_with :deletion, {except: ['spree_countries', 'spree_states']} }
+ config.before(:each) { DatabaseCleaner.strategy = :transaction }
+ config.before(:each, js: true) { DatabaseCleaner.strategy = :deletion, {except: ['spree_countries', 'spree_states']} }
+ config.before(:each) { DatabaseCleaner.start }
+ config.after(:each) { DatabaseCleaner.clean }
- if example.metadata[:js]
- DatabaseCleaner.strategy = :deletion, { :except => ['spree_countries', 'spree_states'] }
- else
- DatabaseCleaner.strategy = :transaction
- end
-
- DatabaseCleaner.start
- end
-
- config.after(:each) do
- DatabaseCleaner.clean
- end
+ # Geocoding
+ config.before(:each) { Spree::Address.any_instance.stub(:geocode).and_return([1,1]) }
+ # Helpers
config.include Rails.application.routes.url_helpers
config.include Spree::UrlHelpers
config.include Spree::CheckoutHelpers
@@ -103,7 +97,7 @@ RSpec.configure do |config|
config.include OpenFoodNetwork::DistributionHelper
config.include ActionView::Helpers::DateHelper
- # Factory girl
+ # FactoryGirl
require 'factory_girl_rails'
config.include FactoryGirl::Syntax::Methods
diff --git a/spec/support/matchers/table_matchers.rb b/spec/support/matchers/table_matchers.rb
new file mode 100644
index 0000000000..146002b751
--- /dev/null
+++ b/spec/support/matchers/table_matchers.rb
@@ -0,0 +1,43 @@
+RSpec::Matchers.define :have_table_row do |row|
+
+ match_for_should do |node|
+ @row = row
+
+ false_on_timeout_error do
+ wait_until { rows_under(node).include? row }
+ end
+ end
+
+ match_for_should_not do |node|
+ @row = row
+
+ false_on_timeout_error do
+ # Without this sleep, we trigger capybara's wait when looking up the table, for the full
+ # period of default_wait_time.
+ sleep 0.1
+ wait_until { !rows_under(node).include? row }
+ end
+ end
+
+ failure_message_for_should do |text|
+ "expected to find table row #{@row}"
+ end
+
+ failure_message_for_should_not do |text|
+ "expected not to find table row #{@row}"
+ end
+
+
+ def rows_under(node)
+ node.all('tr').map { |tr| tr.all('th, td').map(&:text) }
+ end
+
+ def false_on_timeout_error
+ yield
+ rescue TimeoutError
+ false
+ else
+ true
+ end
+
+end
diff --git a/spec/support/request/authentication_workflow.rb b/spec/support/request/authentication_workflow.rb
index e26932bfef..add8de9f28 100644
--- a/spec/support/request/authentication_workflow.rb
+++ b/spec/support/request/authentication_workflow.rb
@@ -33,7 +33,8 @@ module AuthenticationWorkflow
:login => 'admin@ofn.org')
admin_user.spree_roles << admin_role
- login_to_admin_as admin_user
+ quick_login_as admin_user
+ visit spree.admin_path
end
def create_enterprise_user(enterprises = [])
@@ -47,10 +48,12 @@ module AuthenticationWorkflow
end
def login_to_admin_as user
+ quick_login_as user
visit spree.admin_path
- fill_in 'spree_user_email', :with => user.email
- fill_in 'spree_user_password', :with => user.password
- click_button 'Login'
+ #visit spree.admin_path
+ #fill_in 'spree_user_email', :with => user.email
+ #fill_in 'spree_user_password', :with => user.password
+ #click_button 'Login'
end
def login_to_consumer_section
diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb
index bdad9e5d49..c87d33149a 100644
--- a/spec/support/request/shop_workflow.rb
+++ b/spec/support/request/shop_workflow.rb
@@ -1,4 +1,12 @@
module ShopWorkflow
+ def add_to_cart
+ first("input.add_to_cart").click
+ end
+
+ def have_price(price)
+ have_selector ".price", text: price
+ end
+
def set_order(order)
ApplicationController.any_instance.stub(:session).and_return({order_id: order.id, access_token: order.token})
end
diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb
index 6add44834e..3a2cb82128 100644
--- a/spec/support/request/web_helper.rb
+++ b/spec/support/request/web_helper.rb
@@ -1,4 +1,22 @@
module WebHelper
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ # By default, Capybara uses a 30 s wait time, which is more reliable for CI, but too slow
+ # for TDD. Use this to make tests fail fast. Usage:
+ #
+ # describe "foo" do
+ # use_short_wait
+ # ...
+ # end
+ def use_short_wait
+ around { |example| Capybara.using_wait_time(2) { example.run } }
+ end
+ end
+
+
def current_path_should_be path
current_path = URI.parse(current_url).path
current_path.should == path
diff --git a/spec/views/admin/json/enterprise_relationships_rabl_spec.rb b/spec/views/admin/json/enterprise_relationships_rabl_spec.rb
new file mode 100644
index 0000000000..9b72a0d45b
--- /dev/null
+++ b/spec/views/admin/json/enterprise_relationships_rabl_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe "admin/json/_enterprise_relationships.json.rabl" do
+ let(:parent) { create(:enterprise) }
+ let(:child) { create(:enterprise) }
+ let(:enterprise_relationship) { create(:enterprise_relationship, parent: parent, child: child) }
+ let(:render) { Rabl.render([enterprise_relationship], 'admin/json/enterprise_relationships', view_path: 'app/views', scope: RablHelper::FakeContext.instance) }
+
+ it "renders a list of enterprise relationships" do
+ render.should have_json_type(Array).at_path ''
+ render.should have_json_type(Object).at_path '0'
+ end
+
+ it "renders enterprise ids" do
+ render.should be_json_eql(parent.id).at_path '0/parent_id'
+ render.should be_json_eql(child.id).at_path '0/child_id'
+ end
+
+ it "renders enterprise names" do
+ render.should be_json_eql(parent.name.to_json).at_path '0/parent_name'
+ render.should be_json_eql(child.name.to_json).at_path '0/child_name'
+ end
+end
diff --git a/vendor/assets/javascripts/mm-foundation-tpls-0.1.0.min.js b/vendor/assets/javascripts/mm-foundation-tpls-0.1.0.min.js
deleted file mode 100644
index 5fa09b885a..0000000000
--- a/vendor/assets/javascripts/mm-foundation-tpls-0.1.0.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * angular-mm-foundation
- * http://madmimi.github.io/angular-foundation/
-
- * Version: 0.1.0 - 2014-02-05
- * License: MIT
- */
-angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.dropdownToggle","mm.foundation.transition","mm.foundation.modal","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(this.groups.indexOf(a),1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position"]).directive("dropdownToggle",["$document","$location","$position",function(a,b,c){var d=null,e=angular.noop;return{restrict:"CA",scope:{dropdownToggle:"@"},link:function(b,f){var g=angular.element(a[0].querySelector(b.dropdownToggle));b.$watch("$location.path",function(){e()}),g.css("display","none").bind("click",function(){e()}),f.bind("click",function(b){var h=f===d;if(b.preventDefault(),b.stopPropagation(),d&&e(),!h&&!f.hasClass("disabled")&&!f.prop("disabled")){g.css("display","block");var i=c.offset(f),j=c.offset(angular.element(g[0].offsetParent));g.css({left:i.left-j.left+"px",top:i.top-j.top+i.height+"px"}),d=f,e=function(b){b&&(b.preventDefault(),b.stopPropagation()),a.unbind("click",e),g.css("display","none"),e=angular.noop,d=null},a.bind("click",e)}})}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0)}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&e.$apply(function(){o.dismiss(b.key)}))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("")(l),f.append(k));var i=angular.element("");i.attr("window-class",b.windowClass),i.attr("index",n.length()-1),i.attr("animate","animate"),i.html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width+10};break;case"bottom":f={top:a.top+a.height+10,left:a.left};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d-10};break;default:f={top:a.top-e-10,left:a.left}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(c.unbind(x.show,k),c.unbind(x.hide,m))},C=function(){};d.$observe(l+"Trigger",function(a){B(),C(),x=n(a),angular.isFunction(x.show)?C=b.$watch(function(){return x.show(b,c,d)},function(a){return g(a?p:q)}):x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var D=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:''}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=this.createRateObjects(angular.isDefined(b.ratingStates)?angular.copy(a.$parent.$eval(b.ratingStates)):new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(a){c.push(a),(1===c.length||a.active)&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect()}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){return parseInt(a.localStorage.getItem("mm.tour.step"),10)}function c(b){d=b,a.localStorage.setItem("mm.tour.step",b)}var d=b(),e={};this.add=function(a,b){e[a]=b},this.has=function(a){return!!e[a]},this.isActive=function(){return d>0},this.current=function(a){return a?void c(d):d},this.start=function(){c(1)},this.next=function(){c(d+1)},this.end=function(){c(0)}}]).directive("stepPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()}}}}]).directive("step",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("step","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'\n {{heading}}\n \n\n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'\n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","\n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'\n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')
-}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'\n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'\n \n
\n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'- \n {{heading}}\n
\n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'\n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","\n')}]);
\ No newline at end of file