Merge branch 'master' into product-amount-units

Conflicts:
	app/assets/javascripts/admin/bulk_product_update.js.coffee
	spec/spec_helper.rb
This commit is contained in:
Rohan Mitchell
2014-01-14 15:34:05 +11:00
88 changed files with 1723 additions and 719 deletions

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
app/assets/images/matte.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -65,6 +65,7 @@ productsApp.directive "ofnToggleVariants", ->
element.addClass "icon-chevron-down"
productsApp.directive "ofnToggleColumn", ->
link: (scope, element, attrs) ->
element.addClass "unselected" unless scope.column.visible
@@ -77,24 +78,6 @@ productsApp.directive "ofnToggleColumn", ->
scope.column.visible = true
element.removeClass "unselected"
productsApp.directive "ofnToggleColumnList", [
"$compile"
($compile) ->
return link: (scope, element, attrs) ->
dialogDiv = element.next()
element.on "click", ->
pos = element.position()
height = element.outerHeight()
dialogDiv.css(
position: "absolute"
top: (pos.top + height) + "px"
left: pos.left + "px"
).toggle()
]
productsApp.directive "datetimepicker", [
"$parse"
($parse) ->
@@ -126,7 +109,7 @@ productsApp.controller "AdminBulkProductsCtrl", [
unit: {name: "Unit", visible: true}
price: {name: "Price", visible: true}
on_hand: {name: "On Hand", visible: true}
available_on: {name: "Available On", visible: true}
available_on: {name: "Available On", visible: false}
$scope.variant_unit_options = [
["Weight (g)", "weight_1"],
@@ -138,6 +121,39 @@ productsApp.controller "AdminBulkProductsCtrl", [
["Items", "items"]
]
$scope.filterableColumns = [
{ name: "Supplier", db_column: "supplier_name" },
{ name: "Name", db_column: "name" }
]
$scope.filterTypes = [
{ name: "Equals", predicate: "eq" },
{ name: "Contains", predicate: "cont" }
]
$scope.optionTabs =
filters: { title: "Filter Products", visible: false }
column_toggle: { title: "Toggle Columns", visible: false }
$scope.visibleTab = { title: "Lala" }
$scope.perPage = 25
$scope.currentPage = 1
$scope.products = []
$scope.filteredProducts = []
$scope.currentFilters = []
$scope.totalCount = -> $scope.filteredProducts.length
$scope.totalPages = -> Math.ceil($scope.totalCount()/$scope.perPage)
$scope.firstVisibleProduct = -> ($scope.currentPage-1)*$scope.perPage+1
$scope.lastVisibleProduct = -> Math.min($scope.totalCount(),$scope.currentPage*$scope.perPage)
$scope.setPage = (page) -> $scope.currentPage = page
$scope.minPage = -> Math.max(1,Math.min($scope.totalPages()-4,$scope.currentPage-2))
$scope.maxPage = -> Math.min($scope.totalPages(),Math.max(5,$scope.currentPage+2))
$scope.$watch ->
$scope.totalPages()
, (newVal, oldVal) ->
$scope.currentPage = Math.max $scope.totalPages(), 1 if newVal != oldVal && $scope.totalPages() < $scope.currentPage
$scope.initialise = (spree_api_key) ->
authorise_api_reponse = ""
dataFetcher("/api/users/authorise_api?token=" + spree_api_key).then (data) ->
@@ -148,14 +164,23 @@ productsApp.controller "AdminBulkProductsCtrl", [
dataFetcher("/api/enterprises/managed?template=bulk_index&q[is_primary_producer_eq]=true").then (data) ->
$scope.suppliers = data
# Need to have suppliers before we get products so we can match suppliers to product.supplier
dataFetcher("/api/products/managed?template=bulk_index;page=1;per_page=500").then (data) ->
$scope.resetProducts data
$scope.fetchProducts()
else if authorise_api_reponse.hasOwnProperty("error")
$scope.api_error_msg = authorise_api_reponse("error")
else
api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access."
$scope.fetchProducts = -> # WARNING: returns a promise
$scope.loading = true
queryString = $scope.currentFilters.reduce (qs,f) ->
return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};"
, ""
return dataFetcher("/api/products/managed?template=bulk_index;page=1;per_page=500;#{queryString}").then (data) ->
$scope.resetProducts data
$scope.loading = false
$scope.resetProducts = (data) ->
$scope.products = data
$scope.dirtyProducts = {}
@@ -214,6 +239,23 @@ productsApp.controller "AdminBulkProductsCtrl", [
onHand = "error"
onHand
$scope.shiftTab = (tab) ->
$scope.visibleTab.visible = false unless $scope.visibleTab == tab
tab.visible = !tab.visible
$scope.visibleTab = tab
$scope.addFilter = (filter) ->
if $scope.filterableColumns.indexOf(filter.property) >= 0
if $scope.filterTypes.indexOf(filter.predicate) >= 0
$scope.currentFilters.push filter
$scope.fetchProducts()
$scope.removeFilter = (filter) ->
index = $scope.currentFilters.indexOf(filter)
if index != -1
$scope.currentFilters.splice index, 1
$scope.fetchProducts()
$scope.editWarn = (product, variant) ->
if ($scope.dirtyProductCount() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or ($scope.dirtyProductCount() == 0)
@@ -265,11 +307,19 @@ productsApp.controller "AdminBulkProductsCtrl", [
$http(
method: "POST"
url: "/admin/products/bulk_update"
data: productsToSubmit
data:
products: productsToSubmit
filters: $scope.currentFilters
).success((data) ->
# TODO: remove this check altogether, need to write controller tests if we want to test this behaviour properly
# Note: Rob implemented subset(), which is a simpler alternative to productsWithoutDerivedAttributes(). However, it
# conflicted with some changes I made before merging my work, so for now I've reverted to the old way of
# doing things. TODO: Review together and decide on strategy here. -- Rohan, 14-1-2014
#if subset($scope.productsWithoutDerivedAttributes(), data)
if angular.toJson($scope.productsWithoutDerivedAttributes($scope.products)) == angular.toJson($scope.productsWithoutDerivedAttributes(data))
$scope.resetProducts data
$scope.displaySuccess()
$timeout -> $scope.displaySuccess()
else
$scope.displayFailure "Product lists do not match."
).error (data, status) ->
@@ -285,8 +335,10 @@ productsApp.controller "AdminBulkProductsCtrl", [
$scope.packProduct product
productsToSubmit = filterSubmitProducts($scope.dirtyProducts)
$scope.updateProducts productsToSubmit
if productsToSubmit.length > 0
$scope.updateProducts productsToSubmit # Don't submit an empty list
else
$scope.setMessage $scope.updateStatusMessage, "No changes to update.", color: "grey", 3000
$scope.packProduct = (product) ->
if product.variant_unit_with_scale
@@ -394,6 +446,10 @@ productsApp.factory "dataFetcher", [
deferred.promise
]
productsApp.filter "rangeArray", ->
return (input,start,end) ->
input.push(i) for i in [start..end]
input
filterSubmitProducts = (productsToFilter) ->
filteredProducts = []
@@ -477,3 +533,11 @@ toObjectWithIDKeys = (array) ->
object[array[i].id].variants = toObjectWithIDKeys(array[i].variants) if array[i].hasOwnProperty("variants") and array[i].variants instanceof Array
object
subset = (bigArray,smallArray) ->
if smallArray instanceof Array && bigArray instanceof Array && smallArray.length > 0
for item in smallArray
return false if angular.toJson(bigArray).indexOf(angular.toJson(item)) == -1
return true
else
return false

View File

@@ -0,0 +1,14 @@
#= require jquery
#= require jquery_ujs
#= require jquery-ui
#= require spin
#= require ../shared/angular
#= require ../shared/angular-resource
#= require foundation
#= require ./shop
#= require_tree .
$ ->
$(document).foundation()

View File

@@ -0,0 +1,4 @@
Shop.controller "OrderCycleCtrl", ($scope, $rootScope, OrderCycle) ->
$scope.order_cycle = OrderCycle.order_cycle
$scope.changeOrderCycle = ->
OrderCycle.push_order_cycle()

View File

@@ -0,0 +1,13 @@
angular.module("Shop").controller "ProductsCtrl", ($scope, $rootScope, Product) ->
$scope.data = Product.data
Product.update()
#$scope.order_cycle = OrderCycle.order_cycle
#$scope.updateProducts = ->
#$scope.products = Product.all()
#$scope.$watch "order_cycle.order_cycle_id", $scope.updateProducts
#$scope.updateProducts()

View File

@@ -0,0 +1,20 @@
Foundation.libs.section.toggle_active = (e)->
$this = $(this)
self = Foundation.libs.section
region = $this.parent()
content = $this.siblings(self.settings.content_selector)
section = region.parent()
settings = $.extend({}, self.settings, self.data_options(section))
prev_active_region = section.children(self.settings.region_selector).filter("." + self.settings.active_class)
#for anchors inside [data-section-title]
e.preventDefault() if not settings.deep_linking and content.length > 0
e.stopPropagation() #do not catch same click again on parent
unless region.hasClass(self.settings.active_class)
prev_active_region.removeClass self.settings.active_class
region.addClass self.settings.active_class
#force resize for better performance (do not wait timer)
self.resize region.find(self.settings.section_selector).not("[" + self.settings.resized_data_attr + "]"), true
else if not settings.one_up# and (self.small(section) or self.is_vertical_nav(section) or self.is_horizontal_nav(section) or self.is_accordion(section))
region.removeClass self.settings.active_class
settings.callback section

View File

@@ -0,0 +1,7 @@
Shop.factory 'OrderCycle', ($resource, Product, orderCycleData) ->
class OrderCycle
@order_cycle = orderCycleData || {orders_close_at: ""}
@push_order_cycle: ->
new $resource("/shop/order_cycle").save {order_cycle_id: @order_cycle.order_cycle_id}, (order_data)->
OrderCycle.order_cycle.orders_close_at = order_data.orders_close_at
Product.update()

View File

@@ -0,0 +1,11 @@
Shop.factory 'Product', ($resource) ->
new class Product
data: {
products: null
}
update: ->
@data.products = $resource("/shop/products").query =>
#console.log @products
@data
all: ->
@data.products || @update()

View File

@@ -0,0 +1,14 @@
window.Shop = angular.module("Shop", ["ngResource", "filters"]).config ($httpProvider) ->
$httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content')
#angular.module('Shop', ['filters'])
angular.module("filters", []).filter "truncate", ->
(text, length, end) ->
text = text || ""
length = 10 if isNaN(length)
end = "..." if end is `undefined`
if text.length <= length or text.length - end.length <= length
text
else
String(text).substring(0, length - end.length) + end

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/

View File

@@ -2,6 +2,59 @@
display: block;
}
div.pagination {
div.pagenav {
margin: 0px;
span.first, span.prev, span.next, span.last {
padding: 5px 0px;
display:inline-block;
}
}
}
div.pagination_info {
text-align: right;
}
div.applied_filter {
margin-bottom: 5px;
border: solid 2px #5498da;
padding: 5px 0px;
border-radius: 5px;
div.four.columns {
padding-left: 10px;
}
}
div.option_tabs {
div.applied_filters, div.filters, div.column_toggle {
margin-bottom: 10px;
}
}
div.option_tab_titles {
h6 {
border-radius: 3px;
border: 1px solid #cee1f4;
padding: 3px;
text-align: center;
color: darken(#cee1f4, 3);
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
h6.selected {
border: 1px solid #5498da;
color: #5498da;
}
margin-bottom: 10px;
}
tbody.odd {
tr.product { td { background-color: white; } }
tr.variant.odd { td { background-color: lighten(#eff5fc, 3); } }
@@ -27,11 +80,18 @@ li.column-list-item {
border-radius: 3px;
padding: 2px 20px;
margin: 2px 1px;
border: 2px solid darken(#5498da, 3);
border: 2px solid #5498da;
background-color: #5498da;
color: white;
font-size: 100%;
cursor: default;
text-align: center;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
li.column-list-item.unselected {
@@ -42,10 +102,7 @@ li.column-list-item.unselected {
}
ul.column-list {
padding: 5px 8px;
border-radius: 3px;
border: solid 1px darkgray;
list-style:none
list-style: none;
}
table#listing_products.bulk {

View File

@@ -4,4 +4,4 @@
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require_self
*= require_tree .
*/
*/

View File

@@ -0,0 +1,10 @@
/*
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require_self
*= require foundation
*= require_tree .
*/

View File

@@ -0,0 +1,12 @@
@import variables
#footer
padding: 74px 0px
background: $fawn
margin-top: 85px
#copyright
clear: both
img
display: block
margin: 0px auto 8px

View File

@@ -0,0 +1,3 @@
/*body { background: #ff0000; }*/
nav.top-bar
margin-bottom: 0px

View File

@@ -0,0 +1,8 @@
@import typography
@mixin big-input
border: 1px solid #999
font-size: 18px
@extend .avenir
padding: 18px
margin-bottom: 1.25em

View File

@@ -0,0 +1,2 @@
.row
max-width: 74em

View File

@@ -0,0 +1,104 @@
@import mixins
@import variables
product
display: block
shop
#search
font-size: 2em
@include big-input
color: #666
display: block
navigation
display: block
background: $fawn
distributor.details
box-sizing: border-box
display: block
height: 150px
padding: 40px 0px 0px
select
width: 200px
position: relative
img
display: block
height: 100px
width: 100px
margin-right: 12px
location
font-family: "AvenirBla_IE", "AvenirBla"
padding-right: 16px
ordercycle
display: block
position: absolute
right: 0px
top: 40px
form.custom
width: 400px
text-align: right
& > strong
line-height: 2.5
font-size: 1.29em
padding-right: 14px
.custom.dropdown
width: 280px
display: inline-block
background: transparent
border-width: 2px
border-color: #666666
font-size: 1.28em
margin-bottom: 0
closing
font-size: 0.875em
display: block
float: right
padding-top: 14px
tabs
background: url("/assets/matte.png") top left repeat
display: block
.section-container.auto
margin-bottom: 0
& > section
transition:height 0s linear 0.5s
& > .content
background: none
border: none
& > .title, &.active > .title
text-transform: uppercase
line-height: 50px
border: none
&, &:hover
background: none
a
padding: 0px 2.2em
products
display: block
padding-top: 36px
table
width: 100%
border-collapse: collapse
border: none
th
line-height: 50px
.notes
max-width: 300px
td, th
background: #fff
border: 1px solid #cccccc
border-left: 0px
border-right: 0px
td
padding: 20px 0px
input[type=number]
width: 60px
margin: 0px
display: block
float: right
padding-top: 14px

View File

@@ -0,0 +1,36 @@
@font-face
font-family: 'AvenirBla_IE'
src: url("/AveniBla.eot") format("opentype")
@font-face
font-family: 'AvenirBla'
src: url("/AvenirLTStd-Black.otf") format("opentype")
@font-face
font-family: 'AvenirMed_IE'
src: url("/AveniMed.eot") format("opentype")
@font-face
font-family: 'AvenirMed'
src: url("/AvenirLTStd-Medium.otf") format("opentype")
//body
//font-family: "AvenirBla_IE", "AvenirBla"
h1, h2, h3, h4, h5, h6, .avenir
color: #333333
font-family: "AvenirBla_IE", "AvenirBla"
padding: 0px
strong.avenir
font-weight: normal // Avenir is basically bold anyway
td
font-family: "helvetica"
// These selectors match the default Foundation selectors
// For clean overriden magic
table tr th, table tr td
color: #333333
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td
color: #333333

View File

@@ -0,0 +1 @@
$fawn: #f6efe5

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the distributors controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,5 @@
class DarkswarmController < ApplicationController
def index
end
end

View File

@@ -2,7 +2,6 @@ class EnterprisesController < BaseController
helper Spree::ProductsHelper
include OrderCyclesHelper
def index
@enterprises = Enterprise.all
end
@@ -66,7 +65,6 @@ class EnterprisesController < BaseController
order_cycle_options = OrderCycle.active.with_distributor(distributor)
order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1
order.save!
redirect_to main_app.enterprise_path(distributor)

View File

@@ -0,0 +1,48 @@
class ShopController < BaseController
layout "darkswarm"
before_filter :set_distributor
before_filter :set_order_cycles
def show
# All suppliers of all our products
@producers = Exchange.where(receiver_id: @distributor.id).map{ |ex| ex.variants.map {|v| v.product.supplier }}.flatten.uniq
end
def products
unless @products = current_order_cycle.andand.products_distributed_by(@distributor)
render json: "", status: 404
end
end
def order_cycle
if request.post?
if oc = OrderCycle.with_distributor(@distributor).active.find_by_id(params[:order_cycle_id])
current_order(true).set_order_cycle! oc
render partial: "shop/order_cycle"
else
render status: 404, json: ""
end
else
render partial: "shop/order_cycle"
end
end
private
def set_distributor
unless @distributor = current_distributor
redirect_to root_path
end
end
def set_order_cycles
@order_cycles = OrderCycle.with_distributor(@distributor).active
# And default to the only order cycle if there's only the one
if @order_cycles.count == 1
current_order(true).set_order_cycle! @order_cycles.first
end
end
end

View File

@@ -11,11 +11,16 @@ Spree::Admin::ProductsController.class_eval do
end
def bulk_update
collection_hash = Hash[params[:_json].each_with_index.map { |p,i| [i,p] }]
collection_hash = Hash[params[:products].each_with_index.map { |p,i| [i,p] }]
product_set = Spree::ProductSet.new({:collection_attributes => collection_hash})
params[:filters] ||= {}
bulk_index_query = params[:filters].reduce("") do |string, filter|
"#{string}q[#{filter[:property][:db_column]}_#{filter[:predicate][:predicate]}]=#{filter[:value]};"
end
if product_set.save
redirect_to "/api/products/managed?template=bulk_index&page=1&per_page=500"
redirect_to "/api/products/managed?template=bulk_index;page=1;per_page=500;#{bulk_index_query}"
else
render :nothing => true, :status => 418
end

View File

@@ -5,14 +5,13 @@ Spree::OrdersController.class_eval do
before_filter :update_distribution, :only => :update
before_filter :filter_order_params, :only => :update
# Patch Orders#populate to provide distributor_id and order_cycle_id to OrderPopulator
# Patch Orders#populate to populate multi_cart (if enabled)
def populate
if OpenFoodNetwork::FeatureToggle.enabled? :multi_cart
populate_cart params.slice(:products, :variants, :quantity, :distributor_id, :order_cycle_id)
end
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
if populator.populate(params.slice(:products, :variants, :quantity, :distributor_id, :order_cycle_id))
if populator.populate(params.slice(:products, :variants, :quantity))
fire_event('spree.cart.add')
fire_event('spree.order.contents_changed')
respond_with(@order) do |format|

View File

@@ -2,4 +2,4 @@ class SuburbsController < ActionController::Base
def index
@suburbs = Suburb.matching(params[:term]).order(:name).limit(8)
end
end
end

View File

@@ -54,8 +54,8 @@ module OrderCyclesHelper
OpenFoodNetwork::FeatureToggle.enabled? :order_cycles
end
def pickup_time
current_order_cycle.exchanges.to_enterprises(current_distributor).outgoing.first.pickup_time
def pickup_time(order_cycle = current_order_cycle)
order_cycle.exchanges.to_enterprises(current_distributor).outgoing.first.pickup_time
end
end

View File

@@ -0,0 +1,10 @@
module ShopHelper
def order_cycles_name_and_pickup_times(order_cycles)
order_cycles.map do |oc|
[
pickup_time(oc),
oc.id
]
end
end
end

View File

@@ -8,7 +8,7 @@ class Cart < ActiveRecord::Base
order = create_or_find_order_for_distributor distributor, order_cycle, currency
@populator = Spree::OrderPopulator.new(order, currency)
@populator.populate({ :variants => { variant_id => quantity }, :distributor_id => distributor.id, :order_cycle_id => order_cycle })
@populator.populate({ :variants => { variant_id => quantity } })
end
def create_or_find_order_for_distributor distributor, order_cycle, currency
@@ -17,6 +17,7 @@ class Cart < ActiveRecord::Base
order_for_distributor = Spree::Order.create(:currency => currency, :distributor => distributor)
order_for_distributor.distributor = distributor
order_for_distributor.order_cycle = order_cycle
order_for_distributor.save!
orders << order_for_distributor
end

View File

@@ -1,26 +1,15 @@
Spree::OrderPopulator.class_eval do
def populate_with_distribution_validation(from_hash)
@distributor, @order_cycle = load_distributor_and_order_cycle(from_hash)
@distributor, @order_cycle = distributor_and_order_cycle
# Refactor: We may not need this validation - we can't change distribution here, so
# this validation probably can't fail
if !distribution_can_supply_products_in_cart(@distributor, @order_cycle)
errors.add(:base, "That distributor or order cycle can't supply all the products in your cart. Please choose another.")
end
# Set order distributor and order cycle
@orig_distributor, @orig_order_cycle = orig_distributor_and_order_cycle
cart_distribution_set = false
if valid?
set_cart_distributor_and_order_cycle @distributor, @order_cycle
cart_distribution_set = true
end
populate_without_distribution_validation(from_hash) if valid?
# Undo distribution setting if validation failed when adding a product
if !valid? && cart_distribution_set
set_cart_distributor_and_order_cycle @orig_distributor, @orig_order_cycle
end
valid?
end
alias_method_chain :populate, :distribution_validation
@@ -32,7 +21,7 @@ Spree::OrderPopulator.class_eval do
variant = Spree::Variant.find(variant_id)
if quantity > 0
if check_stock_levels(variant, quantity) &&
check_distribution_provided_for(variant) &&
check_order_cycle_provided_for(variant) &&
check_variant_available_under_distribution(variant)
@order.add_variant(variant, quantity, currency)
@@ -43,44 +32,18 @@ Spree::OrderPopulator.class_eval do
private
def orig_distributor_and_order_cycle
def distributor_and_order_cycle
[@order.distributor, @order.order_cycle]
end
def load_distributor_and_order_cycle(from_hash)
distributor = from_hash[:distributor_id].present? ?
Enterprise.is_distributor.find(from_hash[:distributor_id]) : nil
order_cycle = from_hash[:order_cycle_id].present? ?
OrderCycle.find(from_hash[:order_cycle_id]) : nil
[distributor, order_cycle]
end
def set_cart_distributor_and_order_cycle(distributor, order_cycle)
# Using @order.reload or not performing any reload causes totals fields (ie. item_total)
# to be set to zero
@order = Spree::Order.find @order.id
@order.set_distribution! distributor, order_cycle
end
def distribution_can_supply_products_in_cart(distributor, order_cycle)
DistributionChangeValidator.new(@order).can_change_to_distribution?(distributor, order_cycle)
end
def check_distribution_provided_for(variant)
distribution_provided = distribution_provided_for variant
unless distribution_provided
if order_cycle_required_for variant
errors.add(:base, "Please choose a distributor and order cycle for this order.")
else
errors.add(:base, "Please choose a distributor for this order.")
end
end
distribution_provided
def check_order_cycle_provided_for(variant)
order_cycle_provided = (!order_cycle_required_for(variant) || @order_cycle.present?)
errors.add(:base, "Please choose an order cycle for this order.") unless order_cycle_provided
order_cycle_provided
end
def check_variant_available_under_distribution(variant)
@@ -92,10 +55,6 @@ Spree::OrderPopulator.class_eval do
end
end
def distribution_provided_for(variant)
@distributor.present? && (!order_cycle_required_for(variant) || @order_cycle.present?)
end
def order_cycle_required_for(variant)
variant.product.product_distributions.empty?
end

View File

@@ -0,0 +1 @@
TESTING

View File

@@ -15,4 +15,4 @@
%label
= f.check_box :remember_me
= f.label :remember_me, t(:remember_me)
%p= f.submit t(:login), :class => 'button primary', :tabindex => 3, :id => "login_spree_user_remember_me"
%p= f.submit t(:login), :class => 'button primary', :tabindex => 3, :id => "login_spree_user_remember_me"

View File

@@ -0,0 +1,26 @@
!!!
%html
%head
%meta{charset: 'utf-8'}/
%meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/
%title= content_for?(:title) ? yield(:title) : 'Welcome to Open Food Network'
= favicon_link_tag "favicon.png"
= stylesheet_link_tag "darkswarm/all"
= javascript_include_tag "darkswarm/all"
= render "layouts/bugherd_script"
= csrf_meta_tags
%body.off-canvas
= render partial: "shared/menu"
%section{ role: "main" }
= yield
%section#sidebar{ role: "complementary" }
= render partial: "shared/login_panel"
= yield :sidebar
= yield :scripts

View File

@@ -0,0 +1,13 @@
%table#product-list
%thead
%th Item
%th Description
%th Variant
%th Quantity
%th Available?
%th Price
- list.each do |product|
%tr
%td= product.name
%td= product.description

View File

@@ -0,0 +1,3 @@
#copyright.text-center
%img.copyright{src: "/assets/logo.png", alt: "Open Food Network"}
&copy; Copyright 2013 Open Food Foundation

View File

@@ -0,0 +1,12 @@
- if spree_current_user.nil?
%li#login-link= link_to "Login", "#sidebar", id: "sidebarLoginButton", class: "sidebar-button"
%li#login-name.hide
%li.divider
%li#sign-up-link= link_to "Sign Up", "#sidebar", id: "sidebarSignUpButton", class: "sidebar-button"
%li#sign-out-link.hide= link_to "Sign Out", "/logout"
- else
%li#login-link.hide= link_to "Login", "#sidebar", id: "sidebarLoginButton", class: "sidebar-button"
%li#login-name= link_to "#{spree_current_user.email}", "#"
%li.divider
%li#sign-up-link.hide= link_to "Sign Up", "#"
%li#sign-out-link= link_to "Sign Out", "/logout"

View File

@@ -0,0 +1,5 @@
.login-panel
#login-content.hide
= render "home/login"
#sign-up-content.hide
= render "home/signup"

View File

@@ -0,0 +1,11 @@
%nav.top-bar
%section.top-bar-section
%ul.left
%li= link_to image_tag("ofn_logo_small.png"), new_landing_page_path
%li.divider
= render partial: "shared/login"
%ul.right
%li= link_to "Distributors", "#", :data => { "reveal-id" => "become-distributor" }
%li.divider
%li= link_to "Farmers", "#", :data => { "reveal-id" => "become-farmer" }

View File

@@ -0,0 +1,3 @@
.about.right.text-right.small-2.large-3.columns
%h3 About Us
%p= @distributor.long_description.andand.html_safe

View File

@@ -0,0 +1,8 @@
.contact.small-2.large-3.columns
%h3 Contact
%ul
%li= @distributor.email
%li= @distributor.website
= @distributor.address.address1
= @distributor.address.address2
= @distributor.address.city

View File

@@ -0,0 +1,4 @@
- if most_recently_closed = OrderCycle.most_recently_closed_for(@distributor)
The last cycle closed
= distance_of_time_in_words_to_now most_recently_closed.orders_close_at
ago

View File

@@ -0,0 +1,3 @@
- if next_oc = OrderCycle.first_opening_for(@distributor)
The next cycle opens in
= distance_of_time_in_words_to_now next_oc.orders_open_at

View File

@@ -0,0 +1,3 @@
object current_order_cycle
attributes :orders_close_at
attribute :id => :order_cycle_id

View File

@@ -0,0 +1,28 @@
%ordercycle{"ng-controller" => "OrderCycleCtrl"}
:javascript
angular.module('Shop').value('orderCycleData', #{render "shop/order_cycle"})
- if @order_cycles.empty?
Orders are currently closed for this hub
%p
Please contact your hub directly to see if they accept late orders,
or wait until the next cycle opens.
= render partial: "shop/next_order_cycle"
= render partial: "shop/last_order_cycle"
- else
%form.custom
%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}"}
%closing
-#%img{src: "/icon/goes/here"}
Orders close
%strong {{ order_cycle.orders_close_at | date:'EEEE MM'}}

View File

@@ -0,0 +1,48 @@
%products{"ng-controller" => "ProductsCtrl"}
= form_for :order, :url => populate_orders_path, html: {:class => "custom"} do
%input#search.text{"ng-model" => "query", placeholder: "Search"}
%input.button.right{type: :submit, value: "Check Out"}
%table
%thead
%th{colspan: 2} Item
%th.notes Notes
%th Variant
%th QTY
%th Bulk
%th Price
%tbody{"ng-repeat" => "product in data.products | filter:query"}
%tr.product
%td
%img{"ng-src" => "{{ product.master.images[0].small_url }}"}
{{product.master.images[0].alt}}
%td
%h5
{{ product.name }}
{{ product.supplier.name }}
%td.notes {{ product.description | truncate:80 }}
%td {{ product.master.options_text }}
%td
%input{type: :number, value: 0, min: 0, name: "variants[{{product.master.id}}]"}
%td.group_buy
%span{"ng-show" => "product.group_buy"}
Available
%span{"ng-hide" => "product.group_buy"}
Not available
%td.price
%small from
${{ product.price }}
%tr{"ng-repeat" => "variant in product.variants"}
%td{colspan: 3}
%td {{variant.options_text}}
%td
%input{type: :number, value: 0, min: 0, name: "variants[{{variant.id}}]"}
%td.group_buy
%span{"ng-show" => "product.group_buy"}
Available
%span{"ng-hide" => "product.group_buy"}
Not available
%td.price
%small from ${{variant.price }}
%input.button.right{type: :submit, value: "Check Out"}
-#%pre {{ data.products | json }}

View File

@@ -0,0 +1,25 @@
collection @products
attributes :id, :name, :description, :price, :permalink
child :supplier do
attributes :id, :name, :description
end
child :master => :master do
attributes :id, :is_master, :count_on_hand, :options_text
child :images => :images do
attributes :id, :alt
node do |img|
{:small_url => img.attachment.url(:small, false)}
end
end
end
child :variants => :variants do |variant|
attributes :id, :is_master, :count_on_hand, :options_text
child :images => :images do
attributes :id, :alt
node do |img|
{:small_url => img.attachment.url(:small, false)}
end
end
end

View File

@@ -0,0 +1,50 @@
%shop{"ng-app" => "Shop"}
%navigation
%distributor.details.row
%img.left{src: ""}
%h4
= @distributor.name
%location= @distributor.address.city
%small
%a{href: "/"} Change location
= render partial: "shop/order_cycles"
-#%description
%tabs
.row
.section-container.auto{"data-section" => "", "data-options" => "one_up: false"}
%section
%p.title.avenir{"data-section-title" => ""}
%a{href: "#about"} About Us
.content{"data-section-content" => ""}
%p= @distributor.long_description.andand.html_safe
%section
%p.title.avenir{"data-section-title" => ""}
%a{href: "#producers"} Our Producers
.content{"data-section-content" => ""}
%ul
- for producer in @producers
%li= producer.name
%section
%p.title.avenir{"data-section-title" => ""}
%a{href: "#groups"} Our Groups
.content{"data-section-content" => ""}
%p Groups
%section
%p.title.avenir{"data-section-title" => ""}
%a{href: "#contact"} Contact
.content{"data-section-content" => ""}
%p Contact
%products.row
= render partial: "shop/products"
#footer
%section.row
= render partial: "shop/contact_us"
= render partial: "shop/about_us"
= render partial: "shared/copyright"

View File

@@ -13,20 +13,79 @@
%div{ 'ng-app' => 'bulk_product_update', 'ng-controller' => 'AdminBulkProductsCtrl', 'ng-init' => "initialise('#{@spree_api_key}');" }
%div{ 'ng-app' => 'bulk_product_update', 'ng-controller' => 'AdminBulkProductsCtrl', 'ng-init' => "initialise('#{@spree_api_key}');loading=true;" }
%div{ 'ng-show' => '!spree_api_key_ok' }
{{ api_error_msg }}
%div
%div.options
Filter Results:
%input.search{ 'ng-model' => 'query', :type => 'text', 'placeholder' => 'Search Value' }
%input{ :type => 'button', :value => 'Toggle Columns', 'ofn-toggle-column-list' => true }
%div{ :style => 'display: none;' }
%ul.column-list{ style: 'border: 1px solid darkgray; background-color: white;' }
%li.column-list-item{ 'ofn-toggle-column' => 'column', 'ng-repeat' => 'column in columns' }
{{ column.name }}
%br.clear
%br.clear
%div.option_tab_titles{ :class => "sixteen columns alpha" }
%h6{ :class => "three columns alpha", 'ng-repeat' => "tab in optionTabs", "ng-click" => "shiftTab(tab)", "ng-class" => "tab.visible && 'selected' || !tab.visible && 'unselected'"}
{{ tab.title }}
%div.option_tabs{ :class => "sixteen columns alpha" }
%div.filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible' }
%div{ :class => "four columns alpha" }
Column:
%br.clear
%select.select2.fullwidth{ 'ng-model' => 'filterProperty', :name => "filter_property", 'ng-options' => 'fc.name for fc in filterableColumns' }
%div{ :class => "four columns omega" }
Filter Type:
%br.clear
%select.select2.fullwidth{ 'ng-model' => 'filterPredicate', :name => "filter_predicate", 'ng-options' => 'ft.name for ft in filterTypes' }
%div{ :class => "six columns omega" }
Value:
%br.clear
%input{ :class => "four columns alpha", 'ng-model' => 'filterValue', :name => "filter_value", :type => "text", 'placeholder' => 'Filter Value' }
%div{ :class => "two columns omega" }
&nbsp;
%input.fullwidth{ :name => "add_filter", :value => "Apply Filter", :type => "button", "ng-click" => "addFilter({property:filterProperty,predicate:filterPredicate,value:filterValue})" }
%div.applied_filters{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.filters.visible && currentFilters.length > 0' }
%div.applied_filter{ :class => "sixteen columns alpha", 'ng-repeat' => 'filter in currentFilters' }
%div{ :class => "four columns alpha" }
{{ filter.property.name }}
%div{ :class => "four columns omega" }
{{ filter.predicate.name }}
%div{ :class => "six columns omega" }
{{ filter.value }}
%div{ :class => "two columns omega" }
%a{ 'ng-click' => "removeFilter(filter)" } Remove Filter
%div.column_toggle{ :class => "sixteen columns alpha", "ng-show" => 'optionTabs.column_toggle.visible' }
%ul.column-list{ :class => "sixteen columns alpha" }
%li.column-list-item{ :class => "three columns alpha", 'ofn-toggle-column' => 'column', 'ng-repeat' => 'column in columns' }
{{ column.name }}
%hr
%div.loading{ 'ng-show' => 'loading' }
%h4 Loading Products...
%div{ 'ng-show' => '!loading && products.length == 0' }
%h4{ :style => 'color:red;' } No matching products found.
%div{ 'ng-show' => 'products.length == 500' }
%h6 Search returned too many products to display (500+), please apply more search filters to reduce the number of matching products
%div{ 'ng-hide' => 'loading || products.length == 500 || products.length == 0' }
%div.quick_search{ :class => "five columns omega" }
%input.search{ :class => "four columns alpha", 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => 'Quick Search' }
%div.pagination{ :class => "seven columns omega" }
%div.pagenav{ :class => "two columns alpha" }
%span.first
%a{ 'ng-click' => "currentPage = 1", 'ng-show' => "currentPage > 1" }
&laquo;&nbsp;First
%span.prev
%a{ 'ng-click' => "currentPage = currentPage - 1", 'ng-show' => "currentPage > 1" }
&lsaquo;&nbsp;Prev
%div.pagenav{ :class => "columns omega" }
%span.page{ 'ng-repeat' => "page in [] | rangeArray:minPage():maxPage()", 'ng-class' => "{current: currentPage==page}" }
%a{ 'ng-click' => "setPage(page)" }
{{page}}
%span{ 'ng-show' => "maxPage() < totalPages()" } ...
%div.pagenav{ :class => "two columns omega" }
%span.next
%a{ 'ng-click' => "currentPage = currentPage + 1", 'ng-show' => "currentPage < totalPages()" }
Next&nbsp;&rsaquo;
%span.last
%a{ 'ng-click' => "currentPage = totalPages()", 'ng-show' => "currentPage < totalPages()" }
Last&nbsp;&raquo;
%div.pagination_info{ :class => 'four columns alpha' }
Show&nbsp;
%select{ 'ng-model' => 'perPage', :name => 'perPage', 'ng-options' => 'pp for pp in [25,50,100,200]'}
&nbsp;per page
%br
%span Displaying {{firstVisibleProduct()}}-{{lastVisibleProduct()}} of {{totalCount()}} products
%table.index#listing_products.bulk
%colgroup
%col
@@ -48,7 +107,7 @@
%th{ 'ng-show' => 'columns.on_hand.visible' } On Hand
%th{ 'ng-show' => 'columns.available_on.visible' } Av. On
%th.actions
%tbody{ 'ng-repeat' => 'product in products | filter:query', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
%tbody{ 'ng-repeat' => 'product in filteredProducts = (products | filter:query)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", 'ng-show' => "$index >= perPage*(currentPage-1) && $index < perPage*currentPage" }
%tr.product
%td.left-actions
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' }

View File

@@ -1,3 +1,2 @@
collection @products.order('id ASC')
extends "spree/api/products/bulk_show"
attributes :variant_unit, :variant_unit_scale, :variant_unit_name
extends "spree/api/products/bulk_show"

View File

@@ -1,5 +1,5 @@
object @product
attributes :id, :name, :price, :on_hand
attributes :id, :name, :price, :on_hand, :variant_unit, :variant_unit_scale, :variant_unit_name
node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") }
node( :permalink_live ) { |p| p.permalink }

View File

@@ -13,27 +13,6 @@
- else
= render 'add_to_cart_quantity_fields', product: @product
-#temporary hiding the options to choose order cycles and distributors
%div.cleared.hide
%br
- available_distributors = available_distributors_for(order, @product)
- available_order_cycles = available_order_cycles_for(order, @product)
- if available_distributors.length > 1 || order.andand.distributor.nil?
= render 'add_to_cart_distributor_choice', distributor_collection: available_distributors
- else
- distributor = available_distributors.first
- changing_distributor = distributor != order.andand.distributor
= render 'add_to_cart_distributor_fixed', distributor: distributor, changing_distributor: changing_distributor
- if order_cycles_enabled?
- if available_order_cycles.length > 1 || order.andand.order_cycle.nil?
= render 'add_to_cart_order_cycle_choice', order_cycle_collection: available_order_cycles
- else
- order_cycle = available_order_cycles.first
- changing_order_cycle = order_cycle != order.andand.order_cycle
= render 'add_to_cart_order_cycle_fixed', order_cycle: order_cycle, changing_order_cycle: changing_order_cycle
%br
= button_tag :class => 'large primary', :id => 'add-to-cart-button', :type => :submit do
= t(:add_to_cart)