mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge branch 'master' into delay-devise-emails
Conflicts: script/run_tests.sh
This commit is contained in:
@@ -4,13 +4,15 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$scope.StatusMessage = StatusMessage
|
||||
|
||||
$scope.columns =
|
||||
producer: {name: "Producer", visible: true}
|
||||
name: {name: "Name", visible: true}
|
||||
unit: {name: "Unit", visible: true}
|
||||
price: {name: "Price", visible: true}
|
||||
on_hand: {name: "On Hand", visible: true}
|
||||
category: {name: "Category", visible: false}
|
||||
available_on: {name: "Available On", visible: false}
|
||||
producer: {name: "Producer", visible: true}
|
||||
sku: {name: "SKU", visible: false}
|
||||
name: {name: "Name", visible: true}
|
||||
unit: {name: "Unit", visible: true}
|
||||
price: {name: "Price", visible: true}
|
||||
on_hand: {name: "On Hand", visible: true}
|
||||
category: {name: "Category", visible: false}
|
||||
inherits_properties: {name: "Inherits Properties?", visible: false}
|
||||
available_on: {name: "Available On", visible: false}
|
||||
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
|
||||
@@ -285,6 +287,9 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
filteredMaster ?= { id: product.master.id }
|
||||
filteredMaster.display_as = product.master.display_as
|
||||
|
||||
if product.hasOwnProperty("sku")
|
||||
filteredProduct.sku = product.sku
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("name")
|
||||
filteredProduct.name = product.name
|
||||
hasUpdatableProperty = true
|
||||
@@ -307,6 +312,9 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
if product.hasOwnProperty("category_id")
|
||||
filteredProduct.primary_taxon_id = product.category_id
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("inherits_properties")
|
||||
filteredProduct.inherits_properties = product.inherits_properties
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("available_on")
|
||||
filteredProduct.available_on = product.available_on
|
||||
hasUpdatableProperty = true
|
||||
|
||||
@@ -6,10 +6,10 @@ Darkswarm.directive "activeSelector", ->
|
||||
replace: true
|
||||
templateUrl: 'active_selector.html'
|
||||
link: (scope, elem, attr)->
|
||||
scope.selector.emit = scope.emit
|
||||
elem.bind "click", ->
|
||||
scope.$apply ->
|
||||
scope.selector.active = !scope.selector.active
|
||||
# This function is a convention, e.g. a callback on the scope applied when active changes
|
||||
scope.emit() if scope.emit
|
||||
|
||||
unless scope.readOnly && scope.readOnly()
|
||||
scope.selector.emit = scope.emit
|
||||
elem.bind "click", ->
|
||||
scope.$apply ->
|
||||
scope.selector.active = !scope.selector.active
|
||||
# This function is a convention, e.g. a callback on the scope applied when active changes
|
||||
scope.emit() if scope.emit
|
||||
|
||||
@@ -5,7 +5,7 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)->
|
||||
replace: true
|
||||
scope:
|
||||
objects: "&"
|
||||
activeSelectors: "="
|
||||
activeSelectors: "=?"
|
||||
allSelectors: "=?" # Optional
|
||||
templateUrl: "filter_selector.html"
|
||||
|
||||
@@ -13,6 +13,9 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)->
|
||||
selectors_by_id = {}
|
||||
selectors = null # To get scoping/closure right
|
||||
|
||||
scope.readOnly = ->
|
||||
!attr.activeSelectors?
|
||||
|
||||
scope.emit = ->
|
||||
scope.activeSelectors = selectors.filter (selector)->
|
||||
selector.active
|
||||
@@ -23,10 +26,10 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)->
|
||||
# when data has been loaded, in order to pass
|
||||
# selectors up
|
||||
scope.$on 'loadFilterSelectors', ->
|
||||
scope.allSelectors = scope.selectors()
|
||||
scope.allSelectors = scope.selectors() if attr.allSelectors?
|
||||
|
||||
scope.$watchCollection "selectors()", (newValue, oldValue) ->
|
||||
scope.allSelectors = scope.selectors()
|
||||
scope.allSelectors = scope.selectors() if attr.allSelectors?
|
||||
|
||||
# Build a list of selectors
|
||||
scope.selectors = ->
|
||||
|
||||
@@ -4,6 +4,7 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) ->
|
||||
scope:
|
||||
objects: "&"
|
||||
activeSelectors: "="
|
||||
selectorName: "@activeSelectors"
|
||||
link: (scope,element,attrs) ->
|
||||
scope.fitting = false
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Darkswarm.filter 'propertiesWithValuesOf', ->
|
||||
(objects)->
|
||||
propertiesWithValues = {}
|
||||
for object in objects
|
||||
for property in object.properties_with_values
|
||||
propertiesWithValues[property.id] = property
|
||||
propertiesWithValues
|
||||
@@ -21,6 +21,8 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro
|
||||
for product in @products
|
||||
product.supplier = Enterprises.enterprises_by_id[product.supplier.id]
|
||||
Dereferencer.dereference product.taxons, Taxons.taxons_by_id
|
||||
|
||||
product.properties = angular.copy(product.properties_with_values)
|
||||
Dereferencer.dereference product.properties, Properties.properties_by_id
|
||||
|
||||
# May return different objects! If the variant has already been registered
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
%li{ ng: { class: "{active: selector.active}" } }
|
||||
%a{ ng: { transclude: true, class: "{active: selector.active}" } }
|
||||
%a{ "tooltip" => "{{selector.object.value}}", "tooltip-placement" => "bottom",
|
||||
ng: { transclude: true, class: "{active: selector.active, 'has-tip': selector.object.value}" } }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
%active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } }
|
||||
%render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} }
|
||||
%span {{ selector.object.name }}
|
||||
%div{ style: "display: inline-block" }
|
||||
%active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } }
|
||||
%render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} }
|
||||
%span {{ selector.object.name }}
|
||||
|
||||
@@ -6,24 +6,15 @@
|
||||
%em from
|
||||
%span.avenir {{ enterprise.name }}
|
||||
|
||||
%br
|
||||
|
||||
-# TODO: Add product taxons and properties here
|
||||
-# / TODO: Rob - add in taxons and properties and property pop-overs
|
||||
-# / %render-svg{path: "{{product.primary_taxon.icon}}"}
|
||||
-# .pad-top
|
||||
-# %span.filter-shopfront.taxon-selectors
|
||||
-# %ul.inline-block
|
||||
-# %li
|
||||
-# %a.button.tiny.disabled Grains
|
||||
-# %li
|
||||
-# %a.button.tiny.disabled Dairy
|
||||
-#
|
||||
-# %span.filter-shopfront.property-selectors.pad-top
|
||||
-# %ul.inline-block
|
||||
-# %li
|
||||
-# %a.button.tiny Organic certified
|
||||
-# / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/
|
||||
.filter-shopfront.taxon-selectors.inline-block
|
||||
%ul
|
||||
%filter-selector{ objects: "[product] | taxonsOf" }
|
||||
|
||||
.filter-shopfront.property-selectors.inline-block
|
||||
%ul
|
||||
%filter-selector{ objects: "[product] | propertiesWithValuesOf" }
|
||||
|
||||
%div{"ng-if" => "product.description"}
|
||||
%hr
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
%filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" }
|
||||
|
||||
%li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting" } }
|
||||
%a.dropdown{ data: { dropdown: "show-more" }, ng: { class: "{active: selectedOverFlowSelectors().length > 0}" } }
|
||||
%a.dropdown{ data: { dropdown: "{{ 'show-more-' + selectorName }}" }, ng: { class: "{active: selectedOverFlowSelectors().length > 0}" } }
|
||||
%span
|
||||
+ {{ overFlowSelectors().length }} more
|
||||
%i.ofn-i_052-point-down
|
||||
.f-dropdown.text-right.content#show-more
|
||||
.f-dropdown.text-right.content{ ng: { attr: { id: "{{ 'show-more-' + selectorName }}" } } }
|
||||
%ul
|
||||
%active-selector{ ng: { repeat: "selector in overFlowSelectors()", hide: "selector.fits" } }
|
||||
%render-svg{path: "{{selector.object.icon}}"}
|
||||
%render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"}}
|
||||
%span {{ selector.object.name }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@import animations
|
||||
|
||||
@mixin filter-selector($base-clr, $border-clr, $hover-clr)
|
||||
ul.inline-block
|
||||
&.inline-block, ul.inline-block
|
||||
display: inline-block
|
||||
|
||||
li
|
||||
@@ -14,7 +14,7 @@
|
||||
margin: 0 0 0.25rem 0.25rem
|
||||
&:hover, &:focus
|
||||
background: transparent
|
||||
&.active
|
||||
&.active
|
||||
box-shadow: none
|
||||
|
||||
a, a.button
|
||||
@@ -48,10 +48,10 @@
|
||||
svg
|
||||
path
|
||||
fill: $hover-clr
|
||||
|
||||
|
||||
&.disabled
|
||||
opacity: 0.6
|
||||
|
||||
|
||||
&:hover, &:focus
|
||||
border-color: $border-clr
|
||||
color: $base-clr
|
||||
@@ -118,5 +118,3 @@
|
||||
// Shopfront properties
|
||||
&.property-selectors
|
||||
@include filter-selector(#666, #ccc, #777)
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ Spree::Api::ProductsController.class_eval do
|
||||
end
|
||||
|
||||
def render_paged_products(products)
|
||||
render text: { products: ActiveModel::ArraySerializer.new(products, each_serializer: Spree::Api::ProductSerializer), pages: products.num_pages }.to_json
|
||||
render text: { products: ActiveModel::ArraySerializer.new(products, each_serializer: Api::Admin::ProductSerializer), pages: products.num_pages }.to_json
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ module Admin
|
||||
end
|
||||
|
||||
def admin_inject_products
|
||||
admin_inject_json_ams_array "ofn.admin", "products", @products, Spree::Api::ProductSerializer
|
||||
admin_inject_json_ams_array "ofn.admin", "products", @products, Api::Admin::ProductSerializer
|
||||
end
|
||||
|
||||
def admin_inject_taxons
|
||||
|
||||
@@ -18,14 +18,16 @@ Spree::Product.class_eval do
|
||||
delegate_belongs_to :master, :unit_value, :unit_description
|
||||
delegate :images_attributes=, :display_as=, to: :master
|
||||
|
||||
attr_accessible :supplier_id, :primary_taxon_id, :distributor_ids, :product_distributions_attributes, :group_buy, :group_buy_unit_size
|
||||
attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value, :unit_description, :notes, :images_attributes, :display_as
|
||||
attr_accessible :supplier_id, :primary_taxon_id, :distributor_ids, :product_distributions_attributes
|
||||
attr_accessible :group_buy, :group_buy_unit_size, :unit_description, :notes, :images_attributes, :display_as
|
||||
attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value
|
||||
attr_accessible :inherits_properties, :sku
|
||||
|
||||
validates_associated :master, message: "^Price and On Hand must be valid"
|
||||
validates_presence_of :supplier
|
||||
validates :primary_taxon, presence: { message: "^Product Category can't be blank" }
|
||||
validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category"
|
||||
|
||||
|
||||
validates_presence_of :variant_unit, if: :has_variants?
|
||||
validates_presence_of :variant_unit_scale,
|
||||
if: -> p { %w(weight volume).include? p.variant_unit }
|
||||
@@ -106,19 +108,21 @@ Spree::Product.class_eval do
|
||||
|
||||
# -- Methods
|
||||
|
||||
def properties_h
|
||||
def properties_including_inherited
|
||||
# Product properties override producer properties
|
||||
ps = supplier.producer_properties.inject(product_properties) do |properties, property|
|
||||
if properties.find { |p| p.property.presentation == property.property.presentation }
|
||||
properties
|
||||
else
|
||||
properties + [property]
|
||||
ps = product_properties.all
|
||||
|
||||
if inherits_properties
|
||||
supplier.producer_properties.each do |producer_property|
|
||||
unless ps.find { |product_property| product_property.property.presentation == producer_property.property.presentation }
|
||||
ps << producer_property
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ps.
|
||||
sort_by { |pp| pp.position }.
|
||||
map { |pp| {presentation: pp.property.presentation, value: pp.value} }
|
||||
map { |pp| {id: pp.property.id, name: pp.property.presentation, value: pp.value} }
|
||||
end
|
||||
|
||||
def in_distributor?(distributor)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/ insert_after 'table.index.sortable'
|
||||
|
||||
=f.check_box :inherits_properties
|
||||
=f.label :inherits_properties, "Inherit properties from #{@product.supplier.name}? (unless overridden above)"
|
||||
%br
|
||||
%br
|
||||
|
||||
#inherited_properties
|
||||
%table.index
|
||||
%thead
|
||||
%tr{"data-hook" => "producer_properties_header"}
|
||||
%th= t(:inherited_property)
|
||||
%th= t(:value)
|
||||
%th.actions
|
||||
%tbody#producer_properties{"data-hook" => ""}
|
||||
- @product.supplier.producer_properties.each do |producer_property|
|
||||
%tr
|
||||
%td= producer_property.property.presentation
|
||||
%td= producer_property.value
|
||||
%td.actions
|
||||
|
||||
:coffee
|
||||
$(document).ready ->
|
||||
$("#inherited_properties").toggle $("input#product_inherits_properties").is(':checked')
|
||||
$("input#product_inherits_properties").change ->
|
||||
$("#inherited_properties").toggle $(this).is(':checked')
|
||||
@@ -1,12 +1,12 @@
|
||||
class Spree::Api::ProductSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand
|
||||
class Api::Admin::ProductSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
|
||||
|
||||
attributes :on_hand, :price, :available_on, :permalink_live
|
||||
|
||||
has_one :supplier, key: :producer_id, embed: :id
|
||||
has_one :primary_taxon, key: :category_id, embed: :id
|
||||
has_many :variants, key: :variants, serializer: Spree::Api::VariantSerializer # embed: ids
|
||||
has_one :master, serializer: Spree::Api::VariantSerializer
|
||||
has_many :variants, key: :variants, serializer: Api::Admin::VariantSerializer # embed: ids
|
||||
has_one :master, serializer: Api::Admin::VariantSerializer
|
||||
|
||||
def on_hand
|
||||
object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : "On demand"
|
||||
@@ -1,4 +1,4 @@
|
||||
class Spree::Api::VariantSerializer < ActiveModel::Serializer
|
||||
class Api::Admin::VariantSerializer < ActiveModel::Serializer
|
||||
attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display
|
||||
attributes :on_hand, :price
|
||||
|
||||
@@ -31,17 +31,20 @@ class Api::CachedProductSerializer < ActiveModel::Serializer
|
||||
#delegate :cache_key, to: :object
|
||||
|
||||
attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy,
|
||||
:notes, :description
|
||||
:notes, :description, :properties_with_values
|
||||
|
||||
has_many :variants, serializer: Api::VariantSerializer
|
||||
has_many :taxons, serializer: Api::IdSerializer
|
||||
has_many :properties, serializer: Api::IdSerializer
|
||||
has_many :images, serializer: Api::ImageSerializer
|
||||
|
||||
has_one :supplier, serializer: Api::IdSerializer
|
||||
has_one :primary_taxon, serializer: Api::TaxonSerializer
|
||||
has_one :master, serializer: Api::VariantSerializer
|
||||
|
||||
def properties_with_values
|
||||
object.properties_including_inherited
|
||||
end
|
||||
|
||||
def variants
|
||||
# We use the in_stock? method here instead of the in_stock scope because we need to
|
||||
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
.row
|
||||
= render partial: 'shared/components/filter_controls'
|
||||
-# = render partial: 'shared/components/filter_controls'
|
||||
.small-12.medium-6.columns
|
||||
= render partial: 'shared/components/show_profiles'
|
||||
|
||||
.row.animate-show{"ng-show" => "filtersActive"}
|
||||
.small-12.columns
|
||||
.row.filter-box
|
||||
.small-12.large-9.columns
|
||||
%h5.tdhead
|
||||
.light Filter by
|
||||
Type
|
||||
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5
|
||||
%filter-selector{objects: "Enterprises.hubs | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
|
||||
.small-12.large-3.columns
|
||||
%h5.tdhead
|
||||
.light Filter by
|
||||
Delivery
|
||||
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2
|
||||
%shipping-type-selector{results: "shippingTypes"}
|
||||
|
||||
= render partial: 'shared/components/filter_box'
|
||||
-# .row.animate-show{"ng-show" => "filtersActive"}
|
||||
-# .small-12.columns
|
||||
-# .row.filter-box
|
||||
-# .small-12.large-9.columns
|
||||
-# %h5.tdhead
|
||||
-# .light Filter by
|
||||
-# Type
|
||||
-# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5
|
||||
-# %filter-selector{objects: "Enterprises.hubs | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
|
||||
-# .small-12.large-3.columns
|
||||
-# %h5.tdhead
|
||||
-# .light Filter by
|
||||
-# Delivery
|
||||
-# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2
|
||||
-# %shipping-type-selector{results: "shippingTypes"}
|
||||
-#
|
||||
-# = render partial: 'shared/components/filter_box'
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
.row
|
||||
= render partial: 'shared/components/filter_controls'
|
||||
.small-12.medium-6.columns.text-right
|
||||
|
||||
|
||||
.row.animate-show{"ng-show" => "filtersActive"}
|
||||
.small-12.columns
|
||||
.row.filter-box
|
||||
.small-12.columns
|
||||
%h5.tdhead
|
||||
.light Filter by
|
||||
Type
|
||||
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6
|
||||
%filter-selector{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
|
||||
= render partial: 'shared/components/filter_box'
|
||||
-# .row
|
||||
-# = render partial: 'shared/components/filter_controls'
|
||||
-# .small-12.medium-6.columns.text-right
|
||||
-#
|
||||
-#
|
||||
-# .row.animate-show{"ng-show" => "filtersActive"}
|
||||
-# .small-12.columns
|
||||
-# .row.filter-box
|
||||
-# .small-12.columns
|
||||
-# %h5.tdhead
|
||||
-# .light Filter by
|
||||
-# Type
|
||||
-# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6
|
||||
-# %filter-selector{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
|
||||
-# = render partial: 'shared/components/filter_box'
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
%colgroup
|
||||
%col.actions
|
||||
%col.producer{ ng: { show: 'columns.producer.visible' } }
|
||||
%col.sku{ ng: { show: 'columns.sku.visible' } }
|
||||
%col.name{ ng: { show: 'columns.name.visible' } }
|
||||
%col.unit{ ng: { show: 'columns.unit.visible' } }
|
||||
%col.display_as{ ng: { show: 'columns.unit.visible' } }
|
||||
%col.price{ ng: { show: 'columns.price.visible'} }
|
||||
%col.on_hand{ ng: { show: 'columns.on_hand.visible' } }
|
||||
%col.category{ ng: { show: 'columns.category.visible' } }
|
||||
%col.inherits_properties{ ng: { show: 'columns.inherits_properties.visible' } }
|
||||
%col.available_on{ ng: { show: 'columns.available_on.visible' } }
|
||||
%col.actions
|
||||
%col.actions
|
||||
@@ -16,12 +18,14 @@
|
||||
%tr
|
||||
%th.left-actions
|
||||
%th.producer{ 'ng-show' => 'columns.producer.visible' } Producer
|
||||
%th.sku{ 'ng-show' => 'columns.sku.visible' } SKU
|
||||
%th.name{ 'ng-show' => 'columns.name.visible' } Name
|
||||
%th.unit{ 'ng-show' => 'columns.unit.visible' } Unit / Value
|
||||
%th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As
|
||||
%th.price{ 'ng-show' => 'columns.price.visible' } Price
|
||||
%th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand
|
||||
%th.category{ 'ng-show' => 'columns.category.visible' } Category
|
||||
%th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' } Inherits Properties?
|
||||
%th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On
|
||||
%th.actions
|
||||
%th.actions
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" }
|
||||
%td.producer{ 'ng-show' => 'columns.producer.visible' }
|
||||
%select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
|
||||
%td.sku{ 'ng-show' => 'columns.sku.visible' }
|
||||
%input{ 'ng-model' => "product.sku", :name => 'product_sku', 'ofn-track-product' => 'sku', :type => 'text' }
|
||||
%td.name{ 'ng-show' => 'columns.name.visible' }
|
||||
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
|
||||
%td.unit{ 'ng-show' => 'columns.unit.visible' }
|
||||
@@ -20,6 +22,8 @@
|
||||
%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.category{ 'ng-if' => 'columns.category.visible' }
|
||||
%input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' }
|
||||
%td.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }
|
||||
%input{ 'ng-model' => 'product.inherits_properties', :name => 'inherits_properties', 'ofn-track-product' => 'inherits_properties', type: "checkbox" }
|
||||
%td.available_on{ '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" }
|
||||
%td.actions
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
%a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" }
|
||||
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" }
|
||||
%td{ 'ng-show' => 'columns.producer.visible' }
|
||||
%td{ 'ng-show' => 'columns.sku.visible' }
|
||||
%td{ 'ng-show' => 'columns.name.visible' }
|
||||
%input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" }
|
||||
%td.unit_value{ 'ng-show' => 'columns.unit.visible' }
|
||||
@@ -15,6 +16,7 @@
|
||||
%input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' }
|
||||
%span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' }
|
||||
%td{ 'ng-show' => 'columns.category.visible' }
|
||||
%td{ 'ng-show' => 'columns.inherits_properties.visible' }
|
||||
%td{ 'ng-show' => 'columns.available_on.visible' }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" }
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
SECRET_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
TIMEZONE: Melbourne
|
||||
# Default country for dropdowns etc.
|
||||
DEFAULT_COUNTRY: Australia
|
||||
# Default country for dropdowns etc. See for codes: http://en.wikipedia.org/wiki/ISO_3166-1
|
||||
DEFAULT_COUNTRY_CODE: AU
|
||||
# Locale for translation.
|
||||
LOCALE: en
|
||||
# Spree zone.
|
||||
|
||||
@@ -17,7 +17,7 @@ Spree.config do |config|
|
||||
config.checkout_zone = ENV["CHECKOUT_ZONE"]
|
||||
config.currency = ENV['CURRENCY']
|
||||
if Spree::Country.table_exists?
|
||||
country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"])
|
||||
country = Spree::Country.find_by_iso(ENV['DEFAULT_COUNTRY_CODE'])
|
||||
config.default_country_id = country.id if country.present?
|
||||
else
|
||||
config.default_country_id = 12 # Australia
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = function(config) {
|
||||
|
||||
autoWatch: true,
|
||||
|
||||
browsers: ['Chrome'],
|
||||
browsers: ['PhantomJS'],
|
||||
|
||||
junitReporter: {
|
||||
outputFile: 'log/testacular-unit.xml',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class AddAddressInstancesToExistingEnterpriseGroups < ActiveRecord::Migration
|
||||
def change
|
||||
country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY'])
|
||||
country = Spree::Country.find_by_iso(ENV['DEFAULT_COUNTRY_CODE'])
|
||||
state = country.states.first
|
||||
EnterpriseGroup.all.each do |g|
|
||||
next if g.address.present?
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddInheritsPropertiesToProduct < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spree_products, :inherits_properties, :boolean, null: false, default: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class AddDefaultAndNotNullToProducerPropertiesPosition < ActiveRecord::Migration
|
||||
def change
|
||||
ProducerProperty.where(position: nil).each do |producer_property|
|
||||
producer_property.update_attribute(:position, 0)
|
||||
end
|
||||
|
||||
change_column :producer_properties, :position, :integer, null: false, default: 0
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20150410043302) do
|
||||
ActiveRecord::Schema.define(:version => 20150424025907) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -373,9 +373,9 @@ ActiveRecord::Schema.define(:version => 20150410043302) do
|
||||
t.string "value"
|
||||
t.integer "producer_id"
|
||||
t.integer "property_id"
|
||||
t.integer "position"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.integer "position", :default => 0, :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "producer_properties", ["position"], :name => "index_producer_properties_on_position"
|
||||
@@ -766,6 +766,7 @@ ActiveRecord::Schema.define(:version => 20150410043302) do
|
||||
t.string "variant_unit_name"
|
||||
t.text "notes"
|
||||
t.integer "primary_taxon_id", :null => false
|
||||
t.boolean "inherits_properties", :default => true, :null => false
|
||||
end
|
||||
|
||||
add_index "spree_products", ["available_on"], :name => "index_products_on_available_on"
|
||||
|
||||
27
script/ci/includes.sh
Normal file
27
script/ci/includes.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
function load_environment {
|
||||
source /var/lib/jenkins/.rvm/environments/ruby-1.9.3-p392
|
||||
if [ ! -f config/application.yml ]; then
|
||||
ln -s application.yml.example config/application.yml
|
||||
fi
|
||||
}
|
||||
|
||||
function exit_unless_master_merged {
|
||||
if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` != *origin/master* ]]; then
|
||||
echo "This branch does not have the current master merged. Please merge master and push again."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function drop_and_recreate_database {
|
||||
# Adapted from: http://stackoverflow.com/questions/12924466/capistrano-with-postgresql-error-database-is-being-accessed-by-other-users
|
||||
psql -U openfoodweb postgres <<EOF
|
||||
REVOKE CONNECT ON DATABASE $1 FROM public;
|
||||
ALTER DATABASE $1 CONNECTION LIMIT 0;
|
||||
SELECT pg_terminate_backend(procpid)
|
||||
FROM pg_stat_activity
|
||||
WHERE procpid <> pg_backend_pid()
|
||||
AND datname='$1';
|
||||
DROP DATABASE $1;
|
||||
CREATE DATABASE $1;
|
||||
EOF
|
||||
}
|
||||
27
script/ci/load_staging_baseline.sh
Executable file
27
script/ci/load_staging_baseline.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Every time staging is deployed, we load a baseline data set before running the new code's
|
||||
# migrations. This script loads the baseline data set, after first taking a backup of the
|
||||
# current database.
|
||||
|
||||
set -e
|
||||
cd /home/openfoodweb/apps/openfoodweb/current
|
||||
source ./script/ci/includes.sh
|
||||
|
||||
echo "Stopping unicorn and delayed job..."
|
||||
service unicorn_openfoodweb stop
|
||||
RAILS_ENV=staging script/delayed_job -i 0 stop
|
||||
|
||||
echo "Backing up current data..."
|
||||
mkdir -p db/backup
|
||||
pg_dump -h localhost -U openfoodweb openfoodweb_production |gzip > db/backup/staging-`date +%Y%m%d%H%M%S`.sql.gz
|
||||
|
||||
echo "Loading baseline data..."
|
||||
drop_and_recreate_database "openfoodweb_production"
|
||||
gunzip -c db/backup/staging-baseline.sql.gz |psql -h localhost -U openfoodweb openfoodweb_production
|
||||
|
||||
echo "Restarting unicorn..."
|
||||
service unicorn_openfoodweb start
|
||||
# Delayed job is restarted by monit
|
||||
|
||||
echo "Done!"
|
||||
10
script/ci/merge_branch_to_master.sh
Executable file
10
script/ci/merge_branch_to_master.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
source ./script/ci/includes.sh
|
||||
|
||||
echo "--- Verifying branch is based on current master"
|
||||
exit_unless_master_merged
|
||||
|
||||
echo "--- Pushing branch"
|
||||
echo git push origin $BUILDKITE_COMMIT:master
|
||||
@@ -9,7 +9,7 @@ if [[ "$PROD_TEST" != *production* ]]; then
|
||||
fi
|
||||
|
||||
echo "--- Saving baseline data for staging"
|
||||
ssh ofn-staging2 "/home/openfoodweb/apps/openfoodweb/current/script/save_staging_baseline.sh $BUILDKITE_COMMIT"
|
||||
ssh ofn-staging2 "/home/openfoodweb/apps/openfoodweb/current/script/ci/save_staging_baseline.sh $BUILDKITE_COMMIT"
|
||||
|
||||
echo "--- Pushing to production"
|
||||
[[ $(git push production $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]]
|
||||
19
script/ci/push_to_staging.sh
Executable file
19
script/ci/push_to_staging.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
source ./script/ci/includes.sh
|
||||
|
||||
# Add staging git remote if required
|
||||
ST2_TEST=`git remote | grep -s 'staging2' || true`
|
||||
if [[ "$ST2_TEST" != *staging2* ]]; then
|
||||
git remote add staging2 openfoodweb@ofn-staging2:apps/openfoodweb/current
|
||||
fi
|
||||
|
||||
echo "--- Verifying branch is based on current master"
|
||||
exit_unless_master_merged
|
||||
|
||||
echo "--- Loading baseline data"
|
||||
ssh ofn-staging2 "/home/openfoodweb/apps/openfoodweb/current/script/ci/load_staging_baseline.sh"
|
||||
|
||||
echo "--- Pushing to staging"
|
||||
[[ $(git push staging2 $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]]
|
||||
16
script/ci/run_js_tests.sh
Executable file
16
script/ci/run_js_tests.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "--- Loading environment"
|
||||
source ./script/ci/includes.sh
|
||||
load_environment
|
||||
|
||||
echo "--- Verifying branch is based on current master"
|
||||
exit_unless_master_merged
|
||||
|
||||
echo "--- Bundling"
|
||||
bundle install
|
||||
|
||||
echo "--- Running tests"
|
||||
bundle exec rake karma:run
|
||||
@@ -3,10 +3,11 @@
|
||||
set -e
|
||||
|
||||
echo "--- Loading environment"
|
||||
source /var/lib/jenkins/.rvm/environments/ruby-1.9.3-p392
|
||||
if [ ! -f config/application.yml ]; then
|
||||
ln -s application.yml.example config/application.yml
|
||||
fi
|
||||
source ./script/ci/includes.sh
|
||||
load_environment
|
||||
|
||||
echo "--- Verifying branch is based on current master"
|
||||
exit_unless_master_merged
|
||||
|
||||
echo "--- Bundling"
|
||||
bundle install
|
||||
0
script/save_staging_baseline.sh → script/ci/save_staging_baseline.sh
Normal file → Executable file
0
script/save_staging_baseline.sh → script/ci/save_staging_baseline.sh
Normal file → Executable file
0
script/delayed_job.sh
Normal file → Executable file
0
script/delayed_job.sh
Normal file → Executable file
0
script/prepare_imported_db.rb
Normal file → Executable file
0
script/prepare_imported_db.rb
Normal file → Executable file
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
ST2_TEST=`git remote | grep -s 'staging2' || true`
|
||||
if [[ "$ST2_TEST" != *staging2* ]]; then
|
||||
git remote add staging2 openfoodweb@ofn-staging2:apps/openfoodweb/current
|
||||
fi
|
||||
|
||||
[[ $(git push staging2 $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]]
|
||||
@@ -287,7 +287,7 @@ feature %q{
|
||||
s2 = FactoryGirl.create(:supplier_enterprise)
|
||||
t1 = FactoryGirl.create(:taxon)
|
||||
t2 = FactoryGirl.create(:taxon)
|
||||
p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2)
|
||||
p = FactoryGirl.create(:product, supplier: s1, available_on: Date.today, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2, sku: "OLD SKU")
|
||||
p.price = 10.0
|
||||
p.on_hand = 6;
|
||||
p.save!
|
||||
@@ -299,6 +299,8 @@ feature %q{
|
||||
first("div#columns_dropdown", :text => "COLUMNS").click
|
||||
first("div#columns_dropdown div.menu div.menu_item", text: "Available On").click
|
||||
first("div#columns_dropdown div.menu div.menu_item", text: "Category").click
|
||||
first("div#columns_dropdown div.menu div.menu_item", text: "Inherits Properties?").click
|
||||
first("div#columns_dropdown div.menu div.menu_item", text: "SKU").click
|
||||
|
||||
within "tr#p_#{p.id}" do
|
||||
expect(page).to have_field "product_name", with: p.name
|
||||
@@ -308,6 +310,8 @@ feature %q{
|
||||
expect(page).to have_selector "div#s2id_p#{p.id}_category_id a.select2-choice"
|
||||
expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)"
|
||||
expect(page).to have_field "on_hand", with: "6"
|
||||
expect(page).to have_checked_field "inherits_properties"
|
||||
expect(page).to have_field "product_sku", with: p.sku
|
||||
|
||||
fill_in "product_name", with: "Big Bag Of Potatoes"
|
||||
select s2.name, :from => 'producer_id'
|
||||
@@ -317,6 +321,8 @@ feature %q{
|
||||
select2_select t1.name, from: "p#{p.id}_category_id"
|
||||
fill_in "on_hand", with: "18"
|
||||
fill_in "display_as", with: "Big Bag"
|
||||
uncheck "inherits_properties"
|
||||
fill_in "product_sku", with: "NEW SKU"
|
||||
end
|
||||
|
||||
click_button 'Save Changes'
|
||||
@@ -332,6 +338,8 @@ feature %q{
|
||||
expect(p.price).to eq 20.0
|
||||
expect(p.on_hand).to eq 18
|
||||
expect(p.primary_taxon).to eq t1
|
||||
expect(p.inherits_properties).to be false
|
||||
expect(p.sku).to eq "NEW SKU"
|
||||
end
|
||||
|
||||
scenario "updating a product with a variant unit of 'items'" do
|
||||
|
||||
@@ -69,7 +69,7 @@ describe 'Products service', ->
|
||||
expect(Products.products[0].taxons[1]).toBe taxons[0]
|
||||
|
||||
it "dereferences properties", ->
|
||||
product.properties = [1]
|
||||
product.properties_with_values = [1]
|
||||
$httpBackend.expectGET("/shop/products").respond([product])
|
||||
$httpBackend.flush()
|
||||
expect(Products.products[0].properties[1]).toBe properties[0]
|
||||
|
||||
@@ -8,7 +8,7 @@ module Spree
|
||||
it { should belong_to(:primary_taxon) }
|
||||
it { should have_many(:product_distributions) }
|
||||
end
|
||||
|
||||
|
||||
describe "validations and defaults" do
|
||||
it "is valid when built from factory" do
|
||||
build(:product).should be_valid
|
||||
@@ -122,7 +122,7 @@ module Spree
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
describe "scopes" do
|
||||
describe "in_supplier" do
|
||||
@@ -316,8 +316,9 @@ module Spree
|
||||
it "returns product properties as a hash" do
|
||||
product = create(:simple_product)
|
||||
product.set_property 'Organic Certified', 'NASAA 12345'
|
||||
property = product.properties.last
|
||||
|
||||
product.properties_h.should == [{presentation: 'Organic Certified', value: 'NASAA 12345'}]
|
||||
product.properties_including_inherited.should == [{id: property.id, name: "Organic Certified", value: 'NASAA 12345'}]
|
||||
end
|
||||
|
||||
it "returns producer properties as a hash" do
|
||||
@@ -325,8 +326,9 @@ module Spree
|
||||
product = create(:simple_product, supplier: supplier)
|
||||
|
||||
supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
|
||||
property = supplier.properties.last
|
||||
|
||||
product.properties_h.should == [{presentation: 'Organic Certified', value: 'NASAA 54321'}]
|
||||
product.properties_including_inherited.should == [{id: property.id, name: "Organic Certified", value: 'NASAA 54321'}]
|
||||
end
|
||||
|
||||
it "overrides producer properties with product properties" do
|
||||
@@ -335,8 +337,32 @@ module Spree
|
||||
|
||||
product.set_property 'Organic Certified', 'NASAA 12345'
|
||||
supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
|
||||
property = product.properties.last
|
||||
|
||||
product.properties_h.should == [{presentation: 'Organic Certified', value: 'NASAA 12345'}]
|
||||
product.properties_including_inherited.should == [{id: property.id, name: "Organic Certified", value: 'NASAA 12345'}]
|
||||
end
|
||||
|
||||
context "when product has an inherit_properties value set to true" do
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:simple_product, supplier: supplier, inherits_properties: true) }
|
||||
|
||||
it "inherits producer properties" do
|
||||
supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
|
||||
property = supplier.properties.last
|
||||
|
||||
product.properties_including_inherited.should == [{id: property.id, name: "Organic Certified", value: 'NASAA 54321'}]
|
||||
end
|
||||
end
|
||||
|
||||
context "when product has an inherit_properties value set to true" do
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:simple_product, supplier: supplier, inherits_properties: false) }
|
||||
|
||||
it "does not inherit producer properties" do
|
||||
supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
|
||||
|
||||
product.properties_including_inherited.should == []
|
||||
end
|
||||
end
|
||||
|
||||
it "sorts by position" do
|
||||
@@ -351,10 +377,10 @@ module Spree
|
||||
product.product_properties.create!({property_id: pc.id, value: '3', position: 3}, {without_protection: true})
|
||||
supplier.producer_properties.create!({property_id: pb.id, value: '2', position: 2}, {without_protection: true})
|
||||
|
||||
product.properties_h.should ==
|
||||
[{presentation: 'A', value: '1'},
|
||||
{presentation: 'B', value: '2'},
|
||||
{presentation: 'C', value: '3'}]
|
||||
product.properties_including_inherited.should ==
|
||||
[{id: pa.id, name: "A", value: '1'},
|
||||
{id: pb.id, name: "B", value: '2'},
|
||||
{id: pc.id, name: "C", value: '3'}]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe Spree::Api::ProductSerializer do
|
||||
describe Api::Admin::ProductSerializer do
|
||||
let(:product) { create(:simple_product) }
|
||||
it "serializes a product" do
|
||||
serializer = Spree::Api::ProductSerializer.new product
|
||||
serializer = Api::Admin::ProductSerializer.new product
|
||||
serializer.to_json.should match product.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe Spree::Api::VariantSerializer do
|
||||
describe Api::Admin::VariantSerializer do
|
||||
let(:variant) { create(:variant) }
|
||||
it "serializes a variant" do
|
||||
serializer = Spree::Api::VariantSerializer.new variant
|
||||
serializer = Api::Admin::VariantSerializer.new variant
|
||||
serializer.to_json.should match variant.options_text
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user