Merge branch 'master' into redesign

Conflicts:
	app/assets/javascripts/templates/product_modal.html.haml
This commit is contained in:
Rohan Mitchell
2015-05-13 09:53:20 +10:00
42 changed files with 557 additions and 114 deletions

View File

@@ -23,7 +23,7 @@ GIT
GIT
remote: git://github.com/openfoodfoundation/spree.git
revision: 4e0075b07acb56864aca89eee3d9670136176c23
revision: afcc23e489eb604a3e2651598a7c8364e2acc7b3
branch: 1-3-stable
specs:
spree (1.3.6.beta)
@@ -123,7 +123,7 @@ GEM
active_link_to (1.0.0)
active_model_serializers (0.8.1)
activemodel (>= 3.0)
activemerchant (1.46.0)
activemerchant (1.48.0)
activesupport (>= 3.2.14, < 5.0.0)
builder (>= 2.1.2, < 4.0.0)
i18n (>= 0.6.9)
@@ -181,7 +181,7 @@ GEM
climate_control (0.0.3)
activesupport (>= 3.0)
cliver (0.3.2)
cocaine (0.5.5)
cocaine (0.5.7)
climate_control (>= 0.0.3, < 1.0)
coderay (1.0.9)
coffee-rails (3.2.2)
@@ -191,7 +191,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.3.3)
colorize (0.7.5)
colorize (0.7.7)
columnize (0.3.6)
comfortable_mexican_sofa (1.6.24)
active_link_to (~> 1.0.0)
@@ -497,7 +497,7 @@ GEM
sprockets (>= 2.0.0)
turn (0.8.3)
ansi
tzinfo (0.3.43)
tzinfo (0.3.44)
uglifier (1.2.4)
execjs (>= 0.3.0)
multi_json (>= 1.0.2)

View File

@@ -1,4 +1,3 @@
[![Build status](https://badge.buildkite.com/e18473fffac95ed2735ca75700673bd301cac5cffc64de821a.svg?branch=master)](https://buildkite.com/open-food-foundation/open-food-network)
[![Code Climate](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork.png)](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork)
# Open Food Network

View File

@@ -1,4 +1,4 @@
%div{ style: "display: inline-block" }
%active-selector{ ng: { repeat: "selector in selectors()", show: "ifDefined(selector.fits, true)" } }
%div{bindonce:true, style: "display: inline-block" }
%active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } }
%render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} }
%span {{ selector.object.name }}
%span{"bo-text" => "selector.object.name"}

View File

@@ -1,14 +1,11 @@
%div.contact-container{bindonce: true}
%div.modal-centered{"bo-if" => "enterprise.email || enterprise.website || enterprise.phone"}
%p.modal-header Contact
%p{"ng-if" => "enterprise.phone"}
{{ enterprise.phone }}
%p{"bo-if" => "enterprise.phone", "bo-text" => "enterprise.phone"}
%p.word-wrap{"ng-if" => "enterprise.email"}
%a{"ng-href" => "{{enterprise.email | stripUrl}}", target: "_blank", mailto: true}
%span.email
{{ enterprise.email | stripUrl }}
%a{"bo-href" => "enterprise.email | stripUrl", target: "_blank", mailto: true}
%span.email{"bo-bind" => "enterprise.email | stripUrl"}
%p.word-wrap{"ng-if" => "enterprise.website"}
%a{"ng-href" => "http://{{enterprise.website | stripUrl}}", target: "_blank" }
{{ enterprise.website | stripUrl }}
%a{"bo-href-i" => "http://{{enterprise.website | stripUrl}}", target: "_blank", "bo-bind" => "enterprise.website | stripUrl"}

View File

@@ -1,13 +1,13 @@
.highlight{"ng-class" => "{'is_distributor' : enterprise.is_distributor}"}
.highlight{bindonce: true, "ng-class" => "{'is_distributor' : enterprise.is_distributor}"}
.highlight-top.row
.small-12.medium-7.large-8.columns
%h3{"ng-if" => "enterprise.is_distributor"}
%a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise", bindonce: true}
%a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise"}
%i{"ng-class" => "enterprise.icon_font"}
%span {{ enterprise.name }}
%span{"bo-text" => "enterprise.name"}
%h3{"ng-if" => "!enterprise.is_distributor", "ng-class" => "{'is_producer' : enterprise.is_primary_producer}"}
%i{"ng-class" => "enterprise.icon_font"}
%span {{ enterprise.name }}
%span{"bo-text" => "enterprise.name"}
.small-12.medium-5.large-4.columns.text-right.small-only-text-left
%p {{ [enterprise.address.city, enterprise.address.state_name] | printArray}}
%img.hero-img{"ng-src" => "{{enterprise.promo_image}}"}
%p{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"}
%img.hero-img{"bo-src" => "enterprise.promo_image"}

View File

@@ -1,19 +1,19 @@
%div.modal-centered{"ng-if" => "enterprise.twitter || enterprise.facebook || enterprise.linkedin || enterprise.instagram"}
%div.modal-centered{bindonce: true, "bo-if" => "enterprise.twitter || enterprise.facebook || enterprise.linkedin || enterprise.instagram"}
%p.modal-header Follow
.follow-icons{bindonce: true}
%span{"ng-if" => "enterprise.twitter"}
%a{"ng-href" => "http://twitter.com/{{enterprise.twitter}}", target: "_blank"}
.follow-icons
%span{"bo-if" => "enterprise.twitter"}
%a{"bo-href-i" => "http://twitter.com/{{enterprise.twitter}}", target: "_blank"}
%i.ofn-i_041-twitter
%span{"ng-if" => "enterprise.facebook"}
%a{"ng-href" => "http://{{enterprise.facebook | stripUrl}}", target: "_blank"}
%span{"bo-if" => "enterprise.facebook"}
%a{"bo-href-i" => "http://{{enterprise.facebook | stripUrl}}", target: "_blank"}
%i.ofn-i_044-facebook
%span{"ng-if" => "enterprise.linkedin"}
%a{"ng-href" => "http://{{enterprise.linkedin | stripUrl}}", target: "_blank"}
%span{"bo-if" => "enterprise.linkedin"}
%a{"bo-href-i" => "http://{{enterprise.linkedin | stripUrl}}", target: "_blank"}
%i.ofn-i_042-linkedin
%span{"ng-if" => "enterprise.instagram"}
%a{"ng-href" => "http://instagram.com/{{enterprise.instagram}}", target: "_blank"}
%span{"bo-if" => "enterprise.instagram"}
%a{"bo-href-i" => "http://instagram.com/{{enterprise.instagram}}", target: "_blank"}
%i.ofn-i_043-instagram

View File

@@ -2,7 +2,7 @@
.cta-container.small-12.columns
%label
Shop for
%strong {{enterprise.name}}
%strong{"bo-text" => "enterprise.name"}
products at:
%a.cta-hub{"ng-repeat" => "hub in enterprise.hubs",
"bo-href" => "hub.path",
@@ -10,7 +10,7 @@
"ofn-empties-cart" => "hub"}
%i.ofn-i_033-open-sign{"bo-if" => "hub.active"}
%i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"}
.hub-name {{hub.name}}
.button-address {{ hub.address.city }} , {{hub.address.state_name}}
.hub-name{"bo-text" => "hub.name"}
.button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"}
/ %i.ofn-i_007-caret-right

View File

@@ -19,6 +19,6 @@
"ofn-empties-cart" => "enterprise"}
%i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"}
%i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"}
.hub-name {{enterprise.name}}
.button-address {{ enterprise.address.city }} , {{enterprise.address.state_name}}
.hub-name{"bo-text" => "enterprise.name"}
.button-address{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"}
/ %i.ofn-i_007-caret-right

View File

@@ -1,10 +1,10 @@
.row
.row{bindonce: true}
.columns.small-12.large-6.product-header
%h3 {{product.name}}
%h3{"bo-text" => "product.name"}
%span
%em from
%span {{ enterprise.name }}
%span{"bo-text" => "enterprise.name"}
%br
@@ -18,11 +18,11 @@
%div{"ng-if" => "product.description"}
%hr
%p.text-small {{product.description}}
%p.text-small{"bo-text" => "product.description"}
%hr
.columns.small-12.large-6
%img.product-img{"ng-src" => "{{product.largeImage}}", "ng-if" => "product.largeImage"}
%img.product-img.placeholder{"ng-src" => "/assets/noimage/large.png", "ng-if" => "!product.largeImage"}
%img.product-img{"bo-src" => "product.largeImage", "bo-if" => "product.largeImage"}
%img.product-img.placeholder{"bo-src" => "'/assets/noimage/large.png'", "bo-if" => "!product.largeImage"}
%ng-include{src: "'partials/close.html'"}

View File

@@ -0,0 +1,3 @@
@import 'plugins/font-awesome';
.icon-refund:before { @extend .icon-ok:before }

View File

@@ -0,0 +1,8 @@
Spree::Admin::LineItemsController.class_eval do
private
def load_order
@order = Spree::Order.find_by_number!(params[:order_id])
authorize! :update, @order
end
end

View File

@@ -9,6 +9,13 @@ Spree::Admin::OrdersController.class_eval do
# in an auth failure as the @order object is nil for collection actions
before_filter :check_authorization, :except => :bulk_management
# After updating an order, the fees should be updated as well
# Currently, adding or deleting line items does not trigger updating the
# fees! This is a quick fix for that.
# TODO: update fees when adding/removing line items
# instead of the update_distribution_charge method.
after_filter :update_distribution_charge, :only => :update
respond_override :index => { :html =>
{ :success => lambda {
# Filter orders to only show those distributed by current user (or all for admin user)
@@ -17,4 +24,17 @@ Spree::Admin::OrdersController.class_eval do
page(params[:page]).
per(params[:per_page] || Spree::Config[:orders_per_page])
} } }
# Overwrite to use confirm_email_for_customer instead of confirm_email.
# This uses a new template. See mailers/spree/order_mailer_decorator.rb.
def resend
Spree::OrderMailer.confirm_email_for_customer(@order.id, true).deliver
flash[:success] = t(:order_email_resent)
respond_with(@order) { |format| format.html { redirect_to :back } }
end
def update_distribution_charge
@order.update_distribution_charge!
end
end

View File

@@ -323,7 +323,7 @@ Spree::Admin::ReportsController.class_eval do
sort_by: proc { |payment_state| payment_state } },
{ group_by: proc { |payment| payment.order.distributor },
sort_by: proc { |distributor| distributor.name } },
{ group_by: proc { |payment| payment.payment_method },
{ group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } },
sort_by: proc { |method| method.name } } ]
when "itemised_payment_totals"

View File

@@ -6,15 +6,18 @@ Spree::Admin::VariantsController.class_eval do
@variants = Spree::Variant.ransack(search_params.merge(:m => 'or')).result
if params[:distributor_id].present?
distributor = Enterprise.find params[:distributor_id]
@variants = @variants.in_distributor(distributor)
end
if params[:order_cycle_id].present?
order_cycle = OrderCycle.find params[:order_cycle_id]
@variants = @variants.in_order_cycle(order_cycle)
end
if params[:distributor_id].present?
distributor = Enterprise.find params[:distributor_id]
@variants = @variants.in_distributor(distributor)
# Perform scoping after all filtering is done.
# Filtering could be a problem on scoped variants.
@variants.each { |v| v.scope_to_hub(distributor) }
end
end
def destroy

View File

@@ -144,11 +144,27 @@ class AbilityDecorator
end
can [:admin, :bulk_management], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
can [:admin, :create], Spree::LineItem
can [:destroy], Spree::LineItem do |item|
user.admin? || user.enterprises.include?(order.distributor) || user == order.order_cycle.manager
end
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Payment
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization
can [:destroy], Spree::Adjustment do |adjustment|
# Sharing code with destroying a line item. This should be unified and probably applied for other actions as well.
binding.pry
if user.admin?
true
elsif adjustment.adjustable.instance_of? Spree::Order
order = adjustment.adjustable
user.enterprises.include?(order.distributor) || user == order.order_cycle.manager
elsif adjustment.adjustable.instance_of? Spree::LineItem
order = adjustment.adjustable.order
user.enterprises.include?(order.distributor) || user == order.order_cycle.manager
end
end
can [:create], OrderCycle

View File

@@ -198,6 +198,18 @@ Spree::Order.class_eval do
end
end
# Does this order have shipments that can be shipped?
def ready_to_ship?
self.shipments.any?{|s| s.can_ship?}
end
# Ship all pending orders
def ship
self.shipments.each do |s|
s.ship if s.can_ship?
end
end
def available_shipping_methods(display_on = nil)
Spree::ShippingMethod.all_available(self, display_on)
end

View File

@@ -0,0 +1,52 @@
module Spree
Payment.class_eval do
# Pin payments lacks void and credit methods, but it does have refund
# Here we swap credit out for refund and remove void as a possible action
def actions_with_pin_payment_adaptations
actions = actions_without_pin_payment_adaptations
if payment_method.is_a? Gateway::Pin
actions << 'refund' if actions.include? 'credit'
actions.reject! { |a| ['credit', 'void'].include? a }
end
actions
end
alias_method_chain :actions, :pin_payment_adaptations
def refund!(refund_amount=nil)
protect_from_connection_error do
check_environment
refund_amount = calculate_refund_amount(refund_amount)
if payment_method.payment_profiles_supported?
response = payment_method.refund((refund_amount * 100).round, source, response_code, gateway_options)
else
response = payment_method.refund((refund_amount * 100).round, response_code, gateway_options)
end
record_response(response)
if response.success?
self.class.create({ :order => order,
:source => self,
:payment_method => payment_method,
:amount => refund_amount.abs * -1,
:response_code => response.authorization,
:state => 'completed' }, :without_protection => true)
else
gateway_error(response)
end
end
end
private
def calculate_refund_amount(refund_amount=nil)
refund_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs
refund_amount.to_f
end
end
end

View File

@@ -74,6 +74,12 @@ Spree::Variant.class_eval do
self.option_values.destroy ovs
end
# Used like "product.name - full_name". If called like this, a product with
# name "Bread" would be displayed as one of these:
# Bread - 1kg # if display_name blank
# Bread - Spelt Sourdough, 1kg # if display_name is "Spelt Sourdough, 1kg"
# Bread - 1kg Spelt Sourdough # if unit_to_display is "1kg Spelt Sourdough"
# Bread - Spelt Sourdough (1kg) # if display_name is "Spelt Sourdough" and unit_to_display is "1kg"
def full_name
return unit_to_display if display_name.blank?
return display_name if display_name.downcase.include? unit_to_display.downcase

View File

@@ -0,0 +1,3 @@
/ replace "code[erb-loud]:contains('button t(:update)')"
= button t(:update_and_recalculate_fees), 'icon-refresh'

View File

@@ -0,0 +1,3 @@
/ insert_before "code[erb-loud]:contains('button_link_to t(:resend)')"
- if @order.ready_to_ship?
%li= button_link_to t(:ship), fire_admin_order_url(@order, :e => 'ship'), :method => :put, :data => { :confirm => t(:are_you_sure) }

View File

@@ -0,0 +1,6 @@
/ insert_bottom "[data-hook='admin_orders_index_row_actions']"
-# See also: app/overrides/add_capture_order_shortcut.rb
- if order.ready_to_ship?
- # copied from backend/app/views/spree/admin/payments/_list.html.erb
= link_to_with_icon "icon-road", t(:ship), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)}

View File

@@ -0,0 +1,6 @@
/ insert_bottom "[data-hook='admin_orders_index_rows'] td:nth-child(3)"
- if order.special_instructions.present?
%br
%span{class: "icon-warning-sign with-tip", title: order.special_instructions}
notes

View File

@@ -0,0 +1,6 @@
/ insert_after "code[erb-silent]:contains('content_for :page_title')"
- if @order.bill_address.present?
= @order.bill_address.firstname
= @order.bill_address.lastname
\-

View File

@@ -20,13 +20,11 @@
.row.pad-top{bindonce: true}
.small-12.medium-6.columns
.groups-header
%a{"ng-href" => "/groups/{{group.id}}"}
%a{"bo-href-i" => "/groups/{{group.id}}"}
%i.ofn-i_035-groups
%span.group-name
{{ group.name }}
%span.group-name{"bo-text" => "group.name"}
.small-3.medium-2.columns
%p
{{ group.state }}
%p{"bo-text" => "group.state"}
.small-9.medium-4.columns.groups-icons
%p
%link-to-service.ofn-i_050-mail-circle{service: '""', ref: 'group.email.split("").reverse().join("")', mailto: true}

View File

@@ -5,7 +5,7 @@
.trans-sentence
%span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"}
%render-svg{path: "{{taxon.icon}}"}
{{taxon.name}}
%span{"bo-text" => "taxon.name"}
%div.show-for-medium-up{"bo-if" => "hub.taxons.length==0"}
&nbsp;
.columns.small-12.medium-3.large-2.fat
@@ -25,7 +25,6 @@
%li{"ng-repeat" => "enterprise in hub.producers"}
%enterprise-modal
%i.ofn-i_036-producers
%span
{{ enterprise.name }}
%span{"bo-text" => "enterprise.name"}
%div.show-for-medium-up{"bo-if" => "hub.producers.length==0"}
&nbsp;

View File

@@ -2,20 +2,21 @@
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"}
%i{ng: {class: "hub.icon_font"}}
%span.margin-top.hub-name-listing {{ hub.name | truncate:40}}
%i{bo: {class: "hub.icon_font"}}
%span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"}
.columns.small-4.medium-2.large-2
%span.margin-top {{ hub.address.city }}
%span.margin-top{"bo-text" => "hub.address.city"}
.columns.small-2.medium-1.large-1
%span.margin-top {{ hub.address.state_name | uppercase }}
%span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"}
.columns.small-6.medium-3.large-4.text-right{"bo-if" => "hub.active"}
%a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"}
%i.ofn-i_033-open-sign
%span.margin-top{ bo: { if: "current()" } }
%em Shopping here
%span.margin-top{ bo: { if: "!current()" } } {{ hub.orders_close_at | sensible_timeframe }}
%span.margin-top{ bo: { if: "!current()" } }
%span{"bo-bind" => "hub.orders_close_at | sensible_timeframe"}
.columns.small-6.medium-3.large-4.text-right{"bo-if" => "!hub.active"}
%a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"}
@@ -28,12 +29,12 @@
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"}
%i{ng: {class: "hub.icon_font"}}
%span.margin-top.hub-name-listing {{ hub.name | truncate:40}}
%span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"}
.columns.small-4.medium-2.large-2
%span.margin-top {{ hub.address.city }}
%span.margin-top{"bo-text" => "hub.address.city"}
.columns.small-2.medium-1.large-1
%span.margin-top {{ hub.address.state_name | uppercase }}
%span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"}
.columns.small-6.medium-3.large-4.text-right
%span.margin-top{ bo: { if: "!current()" } }

View File

@@ -1,68 +1,68 @@
.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle()", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle()", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.columns.small-12.medium-7.large-7.fat
/ Will add in long description available once clean up HTML formatting producer.long_description
%div{"bo-if" => "producer.description"}
%label About us
%img.right.show-for-medium-up{"ng-src" => "{{producer.logo}}" }
%p.text-small
{{ producer.description }}
%img.right.show-for-medium-up{"bo-src" => "producer.logo" }
%p.text-small{ "bo-text" => "producer.description"}
%div.show-for-medium-up{"bo-if" => "producer.description.length==0"}
%label &nbsp;
.columns.small-12.medium-5.large-5.fat
%div{"ng-if" => "producer.supplied_taxons"}
%div{"bo-if" => "producer.supplied_taxons"}
%label Shop for
%p.trans-sentence
%span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"}
%render-svg{path: "{{taxon.icon}}"}
{{taxon.name}}
%span{"bo-text" => "taxon.name"}
%div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}
&nbsp;
%div{"ng-if" => "producer.email || producer.website || producer.phone"}
%div{"bo-if" => "producer.email || producer.website || producer.phone"}
%label Contact
%p.word-wrap{"ng-if" => "producer.phone"}
Call {{ producer.phone }}
%p.word-wrap{"bo-if" => "producer.phone"}
Call
%span{"bo-text" => "producer.phone"}
%p.word-wrap{"ng-if" => "producer.email"}
%a{"ng-href" => "{{producer.email | stripUrl}}", target: "_blank", mailto: true}
%span.email {{ producer.email | stripUrl }}
%p.word-wrap{"bo-if" => "producer.email"}
%a{"bo-href" => "producer.email | stripUrl", target: "_blank", mailto: true}
%span.email{"bo-bind" => "producer.email | stripUrl"}
%p.word-wrap{"ng-if" => "producer.website"}
%a{"ng-href" => "http://{{producer.website | stripUrl}}", target: "_blank" }
%span {{ producer.website | stripUrl }}
%p.word-wrap{"bo-if" => "producer.website"}
%a{"bo-href-i" => "http://{{producer.website | stripUrl}}", target: "_blank" }
%span{"bo-bind" => "producer.website | stripUrl"}
%div{"ng-if" => "producer.twitter || producer.facebook || producer.linkedin || producer.instagram"}
%div{"bo-if" => "producer.twitter || producer.facebook || producer.linkedin || producer.instagram"}
%label Follow
.follow-icons{bindonce: true}
%span{"ng-if" => "producer.twitter"}
%a{"ng-href" => "http://twitter.com/{{producer.twitter}}", target: "_blank"}
%span{"bo-if" => "producer.twitter"}
%a{"bo-href-i" => "http://twitter.com/{{producer.twitter}}", target: "_blank"}
%i.ofn-i_041-twitter
%span{"ng-if" => "producer.facebook"}
%a{"ng-href" => "http://{{producer.facebook | stripUrl}}", target: "_blank"}
%span{"bo-if" => "producer.facebook"}
%a{"bo-href-i" => "http://{{producer.facebook | stripUrl}}", target: "_blank"}
%i.ofn-i_044-facebook
%span{"ng-if" => "producer.linkedin"}
%a{"ng-href" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"}
%span{"bo-if" => "producer.linkedin"}
%a{"bo-href-i" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"}
%i.ofn-i_042-linkedin
%span{"ng-if" => "producer.instagram"}
%a{"ng-href" => "http://instagram.com/{{producer.instagram}}", target: "_blank"}
%span{"bo-if" => "producer.instagram"}
%a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"}
%i.ofn-i_043-instagram
.row.active_table_row.pad-top{"ng-show" => "open()", "bo-if" => "producer.hubs"}
.row.active_table_row.pad-top{"ng-if" => "open()", "bo-if" => "producer.hubs"}
.columns.small-12
.row
.columns.small-12.fat
%div{"bo-if" => "producer.name"}
%label
Shop for
%span.turquoise {{ producer.name }}
%span.turquoise{"bo-text" => "producer.name"}
products at:
%div.show-for-medium-up{"bo-if" => "!producer.name"}
&nbsp;
@@ -73,6 +73,6 @@
"bo-class" => "{primary: hub.active, secondary: !hub.active}"}
%i.ofn-i_033-open-sign{"bo-if" => "hub.active"}
%i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"}
.hub-name {{hub.name}}
.button-address {{ [hub.address.city, hub.address.state_name] | printArray }}
.hub-name{"bo-text" => "hub.name"}
.button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"}

View File

@@ -2,19 +2,19 @@
.columns.small-12.medium-4.large-4.skinny-head
%span{"bo-if" => "producer.is_distributor" }
%a.is_distributor{"bo-href" => "producer.path" }
%i{ng: {class: "producer.producer_icon_font"}}
%i{bo: {class: "producer.producer_icon_font"}}
%span.margin-top
%strong {{ producer.name }}
%strong{"bo-text" => "producer.name"}
%span.producer-name{"bo-if" => "!producer.is_distributor" }
%i{ng: {class: "producer.producer_icon_font"}}
%i{bo: {class: "producer.producer_icon_font"}}
%span.margin-top
%strong {{ producer.name }}
%strong{"bo-text" => "producer.name"}
.columns.small-6.medium-3.large-3
%span.margin-top {{ producer.address.city }}
%span.margin-top{"bo-text" => "producer.address.city"}
.columns.small-4.medium-3.large-4
%span.margin-top {{ producer.address.state_name | uppercase }}
%span.margin-top{"bo-bind" => "producer.address.state_name | uppercase"}
.columns.small-2.medium-2.large-1.text-right
%span.margin-top
%i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"}

View File

@@ -7,14 +7,13 @@
.small-10.medium-10.large-11.columns.summary-header
%h3
%a{"ng-click" => "triggerProductModal()"}
{{ product.name }}
%span{"bo-text" => "product.name"}
%i.ofn-i_057-expand
%small
%em from
%span
%enterprise-modal
%i.ofn-i_036-producers
{{ enterprise.name }}
%i.ofn-i_036-producers{"bo-text" => "enterprise.name"}
.small-2.medium-2.large-1.columns.text-center
.taxon-flag
%render-svg{path: "{{product.primary_taxon.icon}}"}

View File

@@ -0,0 +1,33 @@
<script type='text/template' id='variant_autocomplete_template'>
<div class='variant-autocomplete-item'>
<figure class='variant-image'>
{{#if variant.image }}
<img src='{{variant.image}}' />
{{ else }}
<img src='/assets/noimage/mini.png' />
{{/if}}
</figure>
<div class='variant-details'>
<h6 class="variant-name">{{variant.name}}</h6>
<ul>
<li><strong>Producer:</strong> {{variant.producer_name}}</li>
</ul>
<ul class='variant-data'>
<li class='variant-sku'><strong>{{t 'sku'}}:</strong> {{variant.sku}}</li>
<li class='variant-on_hand'><strong>{{t 'on_hand' }}:</strong> {{variant.count_on_hand}}</li>
</ul>
{{#if variant.option_values}}
<ul class='variant-options'>
{{#each variant.option_values}}
<li><strong>{{this.option_type.presentation}}:</strong> {{this.presentation}}</li>
{{/each}}
</ul>
{{/if}}
</div>
</div>
</script>

View File

@@ -0,0 +1,34 @@
#
# overriding spree/core/app/views/spree/admin/variants/search.rabl
#
collection @variants
attributes :sku, :options_text, :count_on_hand, :id, :cost_price
node(:name) do |v|
# TODO: when products must have a unit, full_name will always be present
variant_specific = v.full_name
if variant_specific.present?
"#{v.name} - #{v.full_name}"
else
v.name
end
end
node(:full_name) do |v|
v.full_name
end
node(:producer_name) do |v|
v.product.supplier.name
end
child(:images => :images) do
attributes :mini_url
end
child(:option_values => :option_values) do
child(:option_type => :option_type) do
attributes :name, :presentation
end
attributes :name, :presentation
end

View File

@@ -12,6 +12,13 @@ function exit_unless_master_merged {
fi
}
function succeed_if_master_merged {
if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` == *origin/master* ]]; then
echo "This branch already has the current master merged."
exit 0
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

View File

@@ -1,10 +1,10 @@
#!/bin/bash
set -e
set -ex
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
git push origin $BUILDKITE_COMMIT:master

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -e
source ./script/ci/includes.sh
echo "--- Checking if master has already been merged"
succeed_if_master_merged
echo "--- Merging master into this branch"
git checkout $BUILDKITE_BRANCH
git merge origin/$BUILDKITE_BRANCH
git merge origin/master -m "Auto-merge from CI [skip ci]"
git push origin $BUILDKITE_BRANCH
git checkout origin/$BUILDKITE_BRANCH

View File

@@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -ex
# Add production git remote if required
PROD_TEST=`git remote | grep -s 'production' || true`

View File

@@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -ex
source ./script/ci/includes.sh
# Add staging git remote if required

View File

@@ -16,4 +16,4 @@ echo "--- Loading test database"
bundle exec rake db:test:load
echo "--- Running tests"
bundle exec rspec spec --format progress
bundle exec rspec spec

View File

@@ -0,0 +1,30 @@
require 'spec_helper'
describe Spree::Admin::OrdersController do
let!(:order) { create(:order) }
context "updating an order with line items" do
let(:line_item) { create(:line_item) }
before { login_as_admin }
it "updates distribution charges" do
order.line_items << line_item
order.save
Spree::Order.any_instance.should_receive(:update_distribution_charge!)
spree_put :update, {
id: order,
order: {
number: order.number,
distributor_id: order.distributor_id,
order_cycle_id: order.order_cycle_id,
line_items_attributes: [
{
id: line_item.id,
quantity: line_item.quantity
}
]
}
}
end
end
end

View File

@@ -8,6 +8,7 @@ module Spree
describe "search action" do
let!(:p1) { create(:simple_product, name: 'Product 1') }
let!(:p2) { create(:simple_product, name: 'Product 2') }
let!(:vo) { create(:variant_override, variant: p1.master, hub: d, count_on_hand: 44) }
let!(:d) { create(:distributor_enterprise) }
let!(:oc) { create(:simple_order_cycle, distributors: [d], variants: [p1.master]) }
@@ -16,6 +17,12 @@ module Spree
assigns(:variants).should == [p1.master]
end
it "applies variant overrides" do
spree_get :search, q: 'Prod', distributor_id: d.id.to_s
assigns(:variants).should == [p1.master]
assigns(:variants).first.count_on_hand.should == 44
end
it "filters by order cycle" do
spree_get :search, q: 'Prod', order_cycle_id: oc.id.to_s
assigns(:variants).should == [p1.master]

View File

@@ -10,15 +10,15 @@ feature "Authentication", js: true do
scenario "logging into admin redirects home, then back to admin" do
# This is the first admin spec, so give a little extra load time for slow systems
Capybara.using_wait_time(60) do
Capybara.using_wait_time(120) do
visit spree.admin_path
end
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
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
scenario "viewing my account" do

View File

@@ -186,6 +186,59 @@ describe Spree::Order do
end
end
describe "an order without shipping method" do
let(:order) { create(:order) }
it "cannot be shipped" do
order.ready_to_ship?.should == false
end
end
describe "an unpaid order with a shipment" do
let(:order) { create(:order, shipping_method: shipping_method) }
let(:shipping_method) { create(:shipping_method) }
before do
order.create_shipment!
order.reload
order.state = 'complete'
order.shipment.update!(order)
end
it "cannot be shipped" do
order.ready_to_ship?.should == false
end
end
describe "a paid order without a shipment" do
let(:order) { create(:order) }
before do
order.payment_state = 'paid'
order.state = 'complete'
end
it "cannot be shipped" do
order.ready_to_ship?.should == false
end
end
describe "a paid order with a shipment" do
let(:order) { create(:order, shipping_method: shipping_method) }
let(:shipping_method) { create(:shipping_method) }
before do
order.create_shipment!
order.payment_state = 'paid'
order.state = 'complete'
order.shipment.update!(order)
end
it "can be shipped" do
order.ready_to_ship?.should == true
end
end
describe "getting the shipping tax" do
let(:order) { create(:order, shipping_method: shipping_method) }
let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 50.0)) }

View File

@@ -0,0 +1,128 @@
require 'spec_helper'
module Spree
describe Payment do
describe "available actions" do
context "for most gateways" do
let(:payment) { create(:payment, source: create(:credit_card)) }
it "can capture and void" do
payment.actions.sort.should == %w(capture void).sort
end
describe "when a payment has been taken" do
before do
payment.stub(:state) { 'completed' }
payment.stub(:order) { double(:order, payment_state: 'credit_owed') }
end
it "can void and credit" do
payment.actions.sort.should == %w(void credit).sort
end
end
end
context "for Pin Payments" do
let(:d) { create(:distributor_enterprise) }
let(:pin) { Gateway::Pin.create! name: 'pin', distributor_ids: [d.id]}
let(:payment) { create(:payment, source: create(:credit_card), payment_method: pin) }
it "does not void" do
payment.actions.should_not include 'void'
end
describe "when a payment has been taken" do
before do
payment.stub(:state) { 'completed' }
payment.stub(:order) { double(:order, payment_state: 'credit_owed') }
end
it "can refund instead of crediting" do
payment.actions.should_not include 'credit'
payment.actions.should include 'refund'
end
end
end
end
describe "refunding" do
let(:payment) { create(:payment) }
let(:success) { double(:success? => true, authorization: 'abc123') }
let(:failure) { double(:success? => false) }
it "always checks the environment" do
payment.payment_method.stub(:refund) { success }
payment.should_receive(:check_environment)
payment.refund!
end
describe "calculating refund amount" do
it "returns the parameter amount when given" do
payment.send(:calculate_refund_amount, 123).should === 123.0
end
it "refunds up to the value of the payment when the outstanding balance is larger" do
payment.stub(:credit_allowed) { 123 }
payment.stub(:order) { double(:order, outstanding_balance: 1000) }
payment.send(:calculate_refund_amount).should == 123
end
it "refunds up to the outstanding balance of the order when the payment is larger" do
payment.stub(:credit_allowed) { 1000 }
payment.stub(:order) { double(:order, outstanding_balance: 123) }
payment.send(:calculate_refund_amount).should == 123
end
end
describe "performing refunds" do
before do
payment.stub(:calculate_refund_amount) { 123 }
payment.payment_method.should_receive(:refund).and_return(success)
end
it "performs the refund without payment profiles" do
payment.payment_method.stub(:payment_profiles_supported?) { false }
payment.refund!
end
it "performs the refund with payment profiles" do
payment.payment_method.stub(:payment_profiles_supported?) { true }
payment.refund!
end
end
it "records the response" do
payment.stub(:calculate_refund_amount) { 123 }
payment.payment_method.stub(:refund).and_return(success)
payment.should_receive(:record_response).with(success)
payment.refund!
end
it "records a payment on success" do
payment.stub(:calculate_refund_amount) { 123 }
payment.payment_method.stub(:refund).and_return(success)
payment.stub(:record_response)
expect do
payment.refund!
end.to change(Payment, :count).by(1)
p = Payment.last
p.order.should == payment.order
p.source.should == payment
p.payment_method.should == payment.payment_method
p.amount.should == -123
p.response_code.should == success.authorization
p.state.should == 'completed'
end
it "logs the error on failure" do
payment.stub(:calculate_refund_amount) { 123 }
payment.payment_method.stub(:refund).and_return(failure)
payment.stub(:record_response)
payment.should_receive(:gateway_error).with(failure)
payment.refund!
end
end
end
end