diff --git a/Gemfile.lock b/Gemfile.lock
index 5d4b2a0e4e..8015ed1dde 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/README.markdown b/README.markdown
index f43856daf6..b9aaf8193c 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,4 +1,3 @@
-[](https://buildkite.com/open-food-foundation/open-food-network)
[](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork)
# Open Food Network
diff --git a/app/assets/javascripts/templates/filter_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml
index ecd4fb7d6d..c6990c369f 100644
--- a/app/assets/javascripts/templates/filter_selector.html.haml
+++ b/app/assets/javascripts/templates/filter_selector.html.haml
@@ -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"}
diff --git a/app/assets/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml
index caa3bc5e65..165fd69d80 100644
--- a/app/assets/javascripts/templates/partials/contact.html.haml
+++ b/app/assets/javascripts/templates/partials/contact.html.haml
@@ -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"}
diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml
index 27a57ed90b..c0d9d5d9e0 100644
--- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml
+++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml
@@ -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"}
diff --git a/app/assets/javascripts/templates/partials/follow.html.haml b/app/assets/javascripts/templates/partials/follow.html.haml
index 1f7bd5702b..4e2a00086a 100644
--- a/app/assets/javascripts/templates/partials/follow.html.haml
+++ b/app/assets/javascripts/templates/partials/follow.html.haml
@@ -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
diff --git a/app/assets/javascripts/templates/partials/hub_actions.html.haml b/app/assets/javascripts/templates/partials/hub_actions.html.haml
index 61e74afb42..f5b451ef3c 100644
--- a/app/assets/javascripts/templates/partials/hub_actions.html.haml
+++ b/app/assets/javascripts/templates/partials/hub_actions.html.haml
@@ -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
diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml
index 815200821a..8be5ceddac 100644
--- a/app/assets/javascripts/templates/partials/hub_details.html.haml
+++ b/app/assets/javascripts/templates/partials/hub_details.html.haml
@@ -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
diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml
index 96ca3c4c53..915bf3ea5e 100644
--- a/app/assets/javascripts/templates/product_modal.html.haml
+++ b/app/assets/javascripts/templates/product_modal.html.haml
@@ -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'"}
diff --git a/app/assets/stylesheets/admin/icons.css.scss b/app/assets/stylesheets/admin/icons.css.scss
new file mode 100644
index 0000000000..e7737ce8e9
--- /dev/null
+++ b/app/assets/stylesheets/admin/icons.css.scss
@@ -0,0 +1,3 @@
+@import 'plugins/font-awesome';
+
+.icon-refund:before { @extend .icon-ok:before }
diff --git a/app/controllers/spree/admin/line_items_controller_decorator.rb b/app/controllers/spree/admin/line_items_controller_decorator.rb
new file mode 100644
index 0000000000..ca83baa00b
--- /dev/null
+++ b/app/controllers/spree/admin/line_items_controller_decorator.rb
@@ -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
diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb
index 3e3c255ab3..42e7068afc 100644
--- a/app/controllers/spree/admin/orders_controller_decorator.rb
+++ b/app/controllers/spree/admin/orders_controller_decorator.rb
@@ -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
diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb
index f148456813..92653a5aa9 100644
--- a/app/controllers/spree/admin/reports_controller_decorator.rb
+++ b/app/controllers/spree/admin/reports_controller_decorator.rb
@@ -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"
diff --git a/app/controllers/spree/admin/variants_controller_decorator.rb b/app/controllers/spree/admin/variants_controller_decorator.rb
index acb55327ae..539ce83183 100644
--- a/app/controllers/spree/admin/variants_controller_decorator.rb
+++ b/app/controllers/spree/admin/variants_controller_decorator.rb
@@ -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
diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb
index e66c03aa72..b8539979c2 100644
--- a/app/models/spree/ability_decorator.rb
+++ b/app/models/spree/ability_decorator.rb
@@ -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
diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb
index d5e18c3d9b..0dc6977965 100644
--- a/app/models/spree/order_decorator.rb
+++ b/app/models/spree/order_decorator.rb
@@ -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
diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb
new file mode 100644
index 0000000000..1a80f0269f
--- /dev/null
+++ b/app/models/spree/payment_decorator.rb
@@ -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
diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb
index 86a0b5fd69..03543793b2 100644
--- a/app/models/spree/variant_decorator.rb
+++ b/app/models/spree/variant_decorator.rb
@@ -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
diff --git a/app/overrides/spree/admin/orders/_form/relabel_update_button.html.haml.deface b/app/overrides/spree/admin/orders/_form/relabel_update_button.html.haml.deface
new file mode 100644
index 0000000000..d4d3aefc8d
--- /dev/null
+++ b/app/overrides/spree/admin/orders/_form/relabel_update_button.html.haml.deface
@@ -0,0 +1,3 @@
+/ replace "code[erb-loud]:contains('button t(:update)')"
+
+= button t(:update_and_recalculate_fees), 'icon-refresh'
diff --git a/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface b/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface
new file mode 100644
index 0000000000..47fbdea70a
--- /dev/null
+++ b/app/overrides/spree/admin/orders/edit/add_ship_button.html.haml.deface
@@ -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) }
diff --git a/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface b/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface
new file mode 100644
index 0000000000..cf0b2b0abb
--- /dev/null
+++ b/app/overrides/spree/admin/orders/index/add_ship_shortcut.html.haml.deface
@@ -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)}
diff --git a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface
new file mode 100644
index 0000000000..75a2bc4689
--- /dev/null
+++ b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface
@@ -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
diff --git a/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface b/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface
new file mode 100644
index 0000000000..2e3780460e
--- /dev/null
+++ b/app/overrides/spree/admin/shared/_order_tabs/add_customer_name.html.haml.deface
@@ -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
+ \-
diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml
index ff843d8938..2ead48f883 100644
--- a/app/views/groups/index.html.haml
+++ b/app/views/groups/index.html.haml
@@ -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}
diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml
index b3741e5aec..6796601637 100644
--- a/app/views/home/_fat.html.haml
+++ b/app/views/home/_fat.html.haml
@@ -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"}
.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"}
diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml
index aeaae0cae4..cf001371f7 100644
--- a/app/views/home/_skinny.html.haml
+++ b/app/views/home/_skinny.html.haml
@@ -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()" } }
diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml
index 17304abbe3..a6d6661501 100644
--- a/app/views/producers/_fat.html.haml
+++ b/app/views/producers/_fat.html.haml
@@ -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
.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"}
- %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"}
@@ -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"}
diff --git a/app/views/producers/_skinny.html.haml b/app/views/producers/_skinny.html.haml
index aaa59b20e8..49ab0a35be 100644
--- a/app/views/producers/_skinny.html.haml
+++ b/app/views/producers/_skinny.html.haml
@@ -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()}"}
diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml
index 392d7ef31d..dec398f80f 100644
--- a/app/views/shop/products/_summary.html.haml
+++ b/app/views/shop/products/_summary.html.haml
@@ -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}}"}
diff --git a/app/views/spree/admin/variants/_autocomplete.js.erb b/app/views/spree/admin/variants/_autocomplete.js.erb
new file mode 100644
index 0000000000..7b52c0a716
--- /dev/null
+++ b/app/views/spree/admin/variants/_autocomplete.js.erb
@@ -0,0 +1,33 @@
+
diff --git a/app/views/spree/admin/variants/search.rabl b/app/views/spree/admin/variants/search.rabl
new file mode 100644
index 0000000000..afd3f39ce6
--- /dev/null
+++ b/app/views/spree/admin/variants/search.rabl
@@ -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
diff --git a/script/ci/includes.sh b/script/ci/includes.sh
index ee59a732d7..d7619f7d23 100644
--- a/script/ci/includes.sh
+++ b/script/ci/includes.sh
@@ -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 < 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