Standing Orders can be edited

This commit is contained in:
Rob Harrington
2016-10-19 22:07:32 +11:00
parent 4ad6b1a65e
commit 1b711bcd46
18 changed files with 303 additions and 40 deletions

View File

@@ -11,7 +11,10 @@ angular.module("admin.standingOrders").controller "StandingOrderController", ($s
$scope.save = ->
$scope.standing_order_form.$setPristine()
StandingOrder.save()
if $scope.standingOrder.id?
StandingOrder.update()
else
StandingOrder.create()
$scope.setView = (view) -> $scope.view = view
@@ -22,7 +25,7 @@ angular.module("admin.standingOrders").controller "StandingOrderController", ($s
$scope.estimatedSubtotal = ->
$scope.standingOrder.standing_line_items.reduce (subtotal, item) ->
item.price_estimate * item.quantity
subtotal += item.price_estimate * item.quantity
, 0
$scope.estimatedTotal = ->

View File

@@ -16,7 +16,7 @@ angular.module("admin.standingOrders").factory "StandingOrder", ($injector, $htt
, (response) =>
InfoDialog.open 'error', response.data.errors[0]
save: ->
create: ->
StatusMessage.display 'progress', 'Saving...'
delete @errors[k] for k, v of @errors
@standingOrder.$save().then (response) =>
@@ -24,3 +24,12 @@ angular.module("admin.standingOrders").factory "StandingOrder", ($injector, $htt
, (response) =>
StatusMessage.display 'failure', 'Oh no! I was unable to save your changes.'
angular.extend(@errors, response.data.errors)
update: ->
StatusMessage.display 'progress', 'Saving...'
delete @errors[k] for k, v of @errors
@standingOrder.$update().then (response) =>
StatusMessage.display 'success', 'Saved'
, (response) =>
StatusMessage.display 'failure', 'Oh no! I was unable to save your changes.'
angular.extend(@errors, response.data.errors)

View File

@@ -1,8 +1,10 @@
angular.module("admin.standingOrders").factory 'StandingOrderResource', ($resource) ->
$resource('/admin/standing_orders/:action.json', {}, {
$resource('/admin/standing_orders/:id/:action.json', {}, {
'index':
method: 'GET'
isArray: true
'create':
method: 'POST'
'update':
method: 'PUT'
params:
id: '@id'
})

View File

@@ -12,4 +12,8 @@ input[type='button'], input[type='submit'] {
.select2-container-disabled {
pointer-events: none;
.select2-choice > .select2-chosen {
color: #a1a1a1;
}
}

View File

@@ -2,17 +2,19 @@ require 'open_food_network/permissions'
module Admin
class StandingOrdersController < ResourceController
before_filter :load_shop, only: [:new]
before_filter :load_shops, only: [:index]
before_filter :wrap_nested_attrs, only: [:create]
before_filter :load_form_data, only: [:new, :edit]
before_filter :strip_banned_attrs, only: [:update]
before_filter :wrap_nested_attrs, only: [:create, :update]
respond_to :json
respond_override create: { json: {
success: lambda {
shop, next_oc = @standing_order.shop, @standing_order.schedule.current_or_next_order_cycle
fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc) if shop && next_oc
render_as_json @standing_order, fee_calculator: fee_calculator
},
success: lambda { render_as_json @standing_order, fee_calculator: fee_calculator },
failure: lambda { render json: { errors: json_errors }, status: :unprocessable_entity }
} }
respond_override update: { json: {
success: lambda { render_as_json @standing_order, fee_calculator: fee_calculator },
failure: lambda { render json: { errors: json_errors }, status: :unprocessable_entity }
} }
@@ -24,13 +26,8 @@ module Admin
end
def new
@standing_order.shop = @shop
@standing_order.bill_address = Spree::Address.new
@standing_order.ship_address = Spree::Address.new
@customers = Customer.of(@shop)
@schedules = Schedule.with_coordinator(@shop)
@payment_methods = Spree::PaymentMethod.for_distributor(@shop)
@shipping_methods = Spree::ShippingMethod.for_distributor(@shop)
end
private
@@ -49,14 +46,24 @@ module Admin
end
end
def load_shop
@shop = Enterprise.find(params[:shop_id])
end
def load_shops
@shops = Enterprise.managed_by(spree_current_user).is_distributor
end
def load_form_data
@customers = Customer.of(@standing_order.shop)
@schedules = Schedule.with_coordinator(@standing_order.shop)
@payment_methods = Spree::PaymentMethod.for_distributor(@standing_order.shop)
@shipping_methods = Spree::ShippingMethod.for_distributor(@standing_order.shop)
@fee_calculator = fee_calculator
end
def fee_calculator
shop, next_oc = @standing_order.shop, @standing_order.schedule.andand.current_or_next_order_cycle
return nil unless shop && next_oc
OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc)
end
def json_errors
@object.errors.messages.inject({}) do |errors, (k,v)|
errors[k] = v.map{ |msg| @object.errors.full_message(k,msg) }
@@ -80,10 +87,15 @@ module Admin
end
end
def strip_banned_attrs
params[:standing_order].delete :schedule_id
params[:standing_order].delete :customer_id
end
# Overriding Spree method to load data from params here so that
# we can authorise #create using an object with required attributes
def build_resource
StandingOrder.new(shop_id: params[:standing_order].andand[:shop_id])
StandingOrder.new(params[:standing_order])
end
def ams_prefix_whitelist

View File

@@ -120,7 +120,7 @@ module Admin
end
def admin_inject_json_ams(ngModule, name, data, serializer, opts = {})
json = serializer.new(data, scope: spree_current_user).to_json
json = serializer.new(data, {scope: spree_current_user}.merge(opts)).to_json
render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json}
end

View File

@@ -253,7 +253,7 @@ class AbilityDecorator
can [:create], Customer
can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
can [:admin, :new, :index], StandingOrder
can [:create], StandingOrder do |standing_order|
can [:create, :edit, :update], StandingOrder do |standing_order|
user.enterprises.include?(standing_order.shop)
end
can [:admin, :build], StandingLineItem

View File

@@ -1,5 +1,5 @@
class Api::Admin::IndexStandingOrderSerializer < ActiveModel::Serializer
attributes :id, :item_count, :begins_on, :ends_on
attributes :id, :item_count, :begins_on, :ends_on, :edit_path
has_one :shop, serializer: Api::Admin::IdNameSerializer
has_one :customer, serializer: Api::Admin::IdEmailSerializer # Remove IdEmailSerializer if no longer user here
@@ -19,4 +19,8 @@ class Api::Admin::IndexStandingOrderSerializer < ActiveModel::Serializer
def ends_on
object.ends_at.andand.strftime('%a, %b %d, %Y') || I18n.t(:ongoing)
end
def edit_path
edit_admin_standing_order_path(object)
end
end

View File

@@ -1,4 +1,4 @@
= admin_inject_json_ams "admin.standingOrders", "standingOrder", @standing_order, Api::Admin::StandingOrderSerializer if @standing_order
= admin_inject_json_ams "admin.standingOrders", "standingOrder", @standing_order, Api::Admin::StandingOrderSerializer, fee_calculator: @fee_calculator if @standing_order
= admin_inject_json_ams_array "admin.standingOrders", "shops", @shops, Api::Admin::IdNameSerializer if @shops
= admin_inject_json_ams_array "admin.standingOrders", "customers", @customers, Api::Admin::IdEmailSerializer if @customers
= admin_inject_json_ams_array "admin.standingOrders", "schedules", @schedules, Api::Admin::IdNameSerializer if @schedules

View File

@@ -3,13 +3,13 @@
.row
.seven.columns.alpha.field
%label{ for: 'customer_id'}= t('admin.customer')
%input.ofn-select2.fullwidth#customer_id{ name: 'customer_id', type: 'number', data: 'customers', text: 'email', required: true, placeholder: t('admin.choose'), ng: { model: 'standingOrder.customer_id' } }
%input.ofn-select2.fullwidth#customer_id{ name: 'customer_id', type: 'number', data: 'customers', text: 'email', required: true, placeholder: t('admin.choose'), ng: { model: 'standingOrder.customer_id', disabled: 'standingOrder.id' } }
.error{ ng: { show: 'submitted && standing_order_details_form.customer_id.$error.required' } }= t(:error_required)
.error{ ng: { repeat: 'error in errors.customer', show: 'standing_order_details_form.customer_id.$pristine' } } {{ error }}
.two.columns &nbsp;
.seven.columns.omega.field
%label{ for: 'schedule_id'}= t('admin.schedule')
%input.ofn-select2.fullwidth#schedule_id{ name: 'schedule_id', type: 'number', data: 'schedules', required: true, placeholder: t('admin.choose'), ng: { model: 'standingOrder.schedule_id' } }
%input.ofn-select2.fullwidth#schedule_id{ name: 'schedule_id', type: 'number', data: 'schedules', required: true, placeholder: t('admin.choose'), ng: { model: 'standingOrder.schedule_id', disabled: 'standingOrder.id' } }
.error{ ng: { show: 'submitted && standing_order_details_form.schedule_id.$error.required' } }= t(:error_required)
.error{ ng: { repeat: 'error in errors.schedule', show: 'standing_order_details_form.schedule_id.$pristine'} } {{ error }}

View File

@@ -1,6 +1,6 @@
%form.margin-bottom-50{ name: 'standing_order_form', novalidate: true, ng: { submit: 'save()' } }
%save-bar{ dirty: "standing_order_form.$dirty", persist: 'true', ng: { show: "view == 'review'" } }
%input.red{ type: "submit", value: t('admin.standing_orders.create') }
%input.red{ type: "submit", ng: { value: "standingOrder.id ? '#{t(:save_changes)}' : '#{t('admin.standing_orders.create')}'" } }
.details{ ng: { show: "['details','review'].indexOf(view) >= 0" } }
%ng-form{ name: 'standing_order_details_form', ng: { controller: 'DetailsController' } }

View File

@@ -14,7 +14,7 @@
%span= t(:total)
%th.orders-actions.actions
%tbody
%tr.item{ ng: { repeat: 'item in standingOrder.standing_line_items', class: { even: 'even', odd: 'odd' } } }
%tr.item{ id: "sli_{{$index}}", ng: { repeat: 'item in standingOrder.standing_line_items', class: { even: 'even', odd: 'odd' } } }
%td.description {{ item.description }}
%td.price.align-center {{ item.price_estimate | currency }}
%td.qty

View File

@@ -6,7 +6,7 @@
%col.ends_on{ width: "15%", 'ng-show' => 'columns.ends_on.visible' }
%col.payment_method{ width: "20%", 'ng-show' => 'columns.payment_method.visible' }
%col.shipping_method{ width: "20%", 'ng-show' => 'columns.shipping_method.visible' }
-# %col.actions
%col.actions{ width: "5%" }
%thead
%tr
-# %th.bulk
@@ -25,8 +25,8 @@
= t('admin.payment_method')
%th.shipping_method{ ng: { show: 'columns.shipping_method.visible', } }
= t('admin.shipping_method')
-# %th.actions
-# &nbsp;
%th.actions
&nbsp;
%tr.standing_order{ :id => "so_{{standingOrder.id}}", ng: { repeat: "standingOrder in standingOrders | filter:query", class: { even: "'even'", odd: "'odd'" } } }
%td.customer.text-center{ ng: { show: 'columns.customer.visible', bind: '::standingOrder.customer.email' } }
%td.schedule.text-center{ ng: { show: 'columns.schedule.visible', bind: '::standingOrder.schedule.name' } }
@@ -35,6 +35,6 @@
%td.ends_on.text-center{ ng: { show: 'columns.ends_on.visible', bind: '::standingOrder.ends_on' } }
%td.payment_method{ ng: { show: 'columns.payment_method.visible', bind: '::standingOrder.payment_method.name' } }
%td.shipping_method{ ng: { show: 'columns.shipping_method.visible', bind: '::standingOrder.shipping_method.name' } }
-# %td.actions
-# %a.edit-standing-order.icon-edit.no-text{ ng: { href: '{{standingOrder.edit_path}}'} }
%td.actions
%a.edit-standing-order.icon-edit.no-text{ ng: { href: '{{standingOrder.edit_path}}'} }
-# %a.delete-standing-order.icon-trash.no-text{ ng: { href: '{{standingOrder.delete_path}}'}, data: { method: 'delete', confirm: "Are you sure?" } }

View File

@@ -0,0 +1,9 @@
- content_for :page_title do
=t('admin.standing_orders.edit')
-# - content_for :page_actions do
-# %li= button_link_to "Back to standing orders list", main_app.admin_standing_orders_path, icon: 'icon-arrow-left'
%div{ ng: { app: 'admin.standingOrders', controller: 'StandingOrderController', cloak: true } }
= render 'data'
= render 'form'

View File

@@ -177,7 +177,7 @@ Openfoodnetwork::Application.routes.draw do
resources :schedules, only: [:index, :create, :update, :destroy], format: :json
resources :standing_orders, only: [:index, :new, :create]
resources :standing_orders, only: [:index, :new, :create, :edit, :update]
resources :standing_line_items, only: [], format: :json do
post :build, on: :collection

View File

@@ -87,8 +87,7 @@ describe Admin::StandingOrdersController, type: :controller do
end
it 'loads the preloads the necessary data' do
spree_get :new, shop_id: shop.id
expect(assigns(:shop)).to eq shop
spree_get :new, standing_order: { shop_id: shop.id }
expect(assigns(:standing_order)).to be_a_new StandingOrder
expect(assigns(:standing_order).shop).to eq shop
expect(assigns(:customers)).to include customer1, customer2
@@ -214,4 +213,163 @@ describe Admin::StandingOrdersController, type: :controller do
end
end
end
describe 'edit' do
let!(:user) { create(:user) }
let!(:shop) { create(:distributor_enterprise, owner: user) }
let!(:customer1) { create(:customer, enterprise: shop) }
let!(:customer2) { create(:customer, enterprise: shop) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:standing_order) { create(:standing_order,
shop: shop,
customer: customer1,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method
) }
before do
allow(controller).to receive(:spree_current_user) { user }
end
it 'loads the preloads the necessary data' do
spree_get :edit, id: standing_order.id
expect(assigns(:standing_order)).to eq standing_order
expect(assigns(:customers)).to include customer1, customer2
expect(assigns(:schedules)).to eq [schedule]
expect(assigns(:payment_methods)).to eq [payment_method]
expect(assigns(:shipping_methods)).to eq [shipping_method]
end
end
describe 'update' do
let!(:user) { create(:user) }
let!(:shop) { create(:distributor_enterprise, owner: user) }
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:standing_order) { create(:standing_order,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
standing_line_items: [create(:standing_line_item, variant: variant1, quantity: 2)]
) }
let(:standing_line_item1) { standing_order.standing_line_items.first}
let(:params) { { format: :json, id: standing_order.id, standing_order: {} } }
context 'as an non-manager of the standing order shop' do
before do
allow(controller).to receive(:spree_current_user) { create(:user, enterprises: [create(:enterprise)]) }
end
it 'redirects to unauthorized' do
spree_post :update, params
expect(response).to redirect_to spree.unauthorized_path
end
end
context 'as a manager of the standing_order shop' do
before do
allow(controller).to receive(:spree_current_user) { user }
end
context 'when I submit params containing a new customer or schedule id' do
let!(:new_customer) { create(:customer, enterprise: shop) }
let!(:new_schedule) { create(:schedule, order_cycles: [order_cycle]) }
before do
params[:standing_order].merge!({ schedule_id: new_schedule.id, customer_id: new_customer.id})
end
it 'does not alter customer_id or schedule_id' do
spree_post :update, params
standing_order.reload
expect(standing_order.customer).to eq customer
expect(standing_order.schedule).to eq schedule
end
end
context 'when I submit params containing ids of inaccessible objects' do
# As 'user' I shouldnt be able to associate a standing_order with any of these.
let(:unmanaged_enterprise) { create(:enterprise) }
let(:unmanaged_payment_method) { create(:payment_method, distributors: [unmanaged_enterprise]) }
let(:unmanaged_shipping_method) { create(:shipping_method, distributors: [unmanaged_enterprise]) }
before do
params[:standing_order].merge!({
payment_method_id: unmanaged_payment_method.id,
shipping_method_id: unmanaged_shipping_method.id,
})
end
it 'returns errors' do
expect{ spree_post :update, params }.to_not change{StandingOrder.count}
json_response = JSON.parse(response.body)
expect(json_response['errors'].keys).to include 'payment_method', 'shipping_method'
standing_order.reload
expect(standing_order.payment_method).to eq payment_method
expect(standing_order.shipping_method).to eq shipping_method
end
end
context 'when I submit valid params' do
let!(:new_payment_method) { create(:payment_method, distributors: [shop]) }
let!(:new_shipping_method) { create(:shipping_method, distributors: [shop]) }
before do
params[:standing_order].merge!({payment_method_id: new_payment_method.id, shipping_method_id: new_shipping_method.id})
end
it 'updates the standing order' do
spree_post :update, params
standing_order.reload
expect(standing_order.schedule).to eq schedule
expect(standing_order.customer).to eq customer
expect(standing_order.payment_method).to eq new_payment_method
expect(standing_order.shipping_method).to eq new_shipping_method
end
context 'with standing_line_items params' do
let!(:product2) { create(:product, supplier: shop) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
before do
params[:standing_line_items] = [{id: standing_line_item1.id, quantity: 1, variant_id: variant1.id}, { quantity: 2, variant_id: variant2.id}]
end
context 'where the specified variants are not available from the shop' do
it 'returns an error' do
expect{ spree_post :update, params }.to_not change{standing_order.standing_line_items.count}
json_response = JSON.parse(response.body)
expect(json_response['errors']['base']).to eq ["#{product2.name} - #{variant2.full_name} is not available from the selected schedule"]
end
end
context 'where the specified variants are available from the shop' do
before { outgoing_exchange.update_attributes(variants: [variant1, variant2]) }
it 'creates standing line items for the standing order' do
expect{ spree_post :update, params }.to change{standing_order.standing_line_items.count}.by(1)
standing_order.reload
expect(standing_order.standing_line_items.count).to be 2
standing_line_item = standing_order.standing_line_items.last
expect(standing_line_item.quantity).to be 2
expect(standing_line_item.variant).to eq variant2
end
end
end
end
end
end
end

View File

@@ -149,6 +149,12 @@ FactoryGirl.define do
begins_at { 1.month.ago }
end
factory :standing_line_item, :class => StandingLineItem do
standing_order
variant
quantity 1
end
factory :variant_override, :class => VariantOverride do
price 77.77
count_on_hand 11111

View File

@@ -72,7 +72,7 @@ feature 'Standing Orders' do
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
it "passes the smoke test" do
visit new_admin_standing_order_path(shop_id: shop.id)
visit new_admin_standing_order_path(standing_order: { shop_id: shop.id })
select2_select customer.email, from: 'customer_id'
select2_select schedule.name, from: 'schedule_id'
@@ -148,6 +148,62 @@ feature 'Standing Orders' do
expect(standing_line_item.variant).to eq variant
expect(standing_line_item.quantity).to eq 2
end
context 'editing an existing standing order' do
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:standing_order) { create(:standing_order,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
standing_line_items: [create(:standing_line_item, variant: variant1, quantity: 2)]
) }
it "passes the smoke test" do
visit edit_admin_standing_order_path(standing_order)
# Customer and Schedule cannot be edited
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
# Existing products should be visible
within "#sli_0" do
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
end
# Add variant2 to the standing order
targetted_select2_search product2.name, from: '#add_variant_id', dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 1
click_link 'Add'
within "#sli_1" do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$7.75"
end
click_button 'Save Changes'
expect(page).to have_content 'Saved'
# Total should be $35.25
expect(page).to have_selector '#order_form_total', text: "$35.25"
expect(standing_order.reload.standing_line_items.length).to eq 2
end
end
end
end
end