Tebbers
Tebbers

Reputation: 484

Ruby on Rails - custom PATCH action for form_for

I'm working on a Spree e-commerce store built on Ruby on Rails and want a custom action where a user can mark their order as complete straight from the checkout page without going through delivery etc. I've overridden all the checkout steps but cannot get the 'Checkout' button to complete the order by sending the order to a custom action in the Orders controller.

I'd like to think I've ticked off all the boxes: created a patch action in routes.rb and checked rake routes to make sure the route exists. But it's still telling me there is no route.

The cart page won't even load before I submit anything, with the following error. I've spent all day trying to fix this so any ideas would be great....

The error:

No route matches {:action=>"complete", :controller=>"spree/orders", :method=>:patch}

Routes.rb:

resources :orders do
   member do
     patch 'complete', to: 'orders#complete'
   end
 end

Rake routes:

        Prefix Verb   URI Pattern                    Controller#Action
       spree        /                              Spree::Core::Engine
complete_order PATCH  /orders/:id/complete(.:format) orders#complete
        orders GET    /orders(.:format)              orders#index
               POST   /orders(.:format)              orders#create
     new_order GET    /orders/new(.:format)          orders#new
    edit_order GET    /orders/:id/edit(.:format)     orders#edit
         order GET    /orders/:id(.:format)          orders#show
               PATCH  /orders/:id(.:format)          orders#update
               PUT    /orders/:id(.:format)          orders#update
               DELETE /orders/:id(.:format)          orders#destroy

HTML:

<%= form_for :order, url: {action: 'complete', method: :patch} do |f| %>
  <% f.submit %>
<% end %>

I haven't created the controller yet but it would be:

def complete
  # mark order as complete
  # redirect to confirmation page
end

Would really appreciate any help. Thanks

EDIT: Here is the updated view (app/views/orders/edit.html.erb):

<% @body_id = 'cart' %>
<div data-hook="cart_container">
  <h1><%= Spree.t(:shopping_cart) %></h1>

  <% if @order.line_items.empty? %>
    <div data-hook="empty_cart">
      <div class="alert alert-info"><%= Spree.t(:your_cart_is_empty) %></div>
      <p><%= link_to Spree.t(:continue_shopping), products_path, class: 'btn btn-default' %></p>
    </div>
  <% else %>
    <div data-hook="outside_cart_form">
      <%= form_for @order, url: update_cart_path, html: { id: 'update-cart' } do |order_form| %>
        <div data-hook="inside_cart_form">

          <div data-hook="cart_items" class="table-responsive">
            <%= render partial: 'form', locals: { order_form: order_form } %>
          </div>

        </div>
      <% end %>
    </div>

    <div id="empty-cart" class="col-md-6" data-hook>
      <%= form_tag empty_cart_path, method: :put do %>
        <p id="clear_cart_link" data-hook>
          <%= submit_tag Spree.t(:empty_cart), class: 'btn btn-default' %>
          <%= Spree.t(:or) %>
          <%= link_to Spree.t(:continue_shopping), products_path, class: 'continue' %>
        </p>
      <% end %>
    </div>

    <div id="complete-order">
      complete order here - submit to custom controller
      <%= @order.id  %>
      <%= form_for @order, url: complete_order_path(@order) do |f| %>
        <% f.submit %>
      <% end %>


    </div>

  <% end %>
</div>

Here is the whole controller:

module Spree
  class OrdersController < Spree::StoreController
    before_action :check_authorization
    rescue_from ActiveRecord::RecordNotFound, :with => :render_404
    helper 'spree/products', 'spree/orders'

    respond_to :html

    before_action :assign_order_with_lock, only: :update
    skip_before_action :verify_authenticity_token, only: [:populate]

    def show
      @order = Order.includes(line_items: [variant: [:option_values, :images, :product]], bill_address: :state, ship_address: :state).find_by_number!(params[:id])
    end

    def complete
      @order = current_order
    end

    def update
      if @order.contents.update_cart(order_params)
        respond_with(@order) do |format|
          format.html do
            if params.has_key?(:checkout)
              @order.next if @order.cart?
              redirect_to checkout_state_path(@order.checkout_steps.first)
            else
              redirect_to cart_path
            end
          end
        end
      else
        respond_with(@order)
      end
    end

    # Shows the current incomplete order from the session
    def edit
      @order = current_order || Order.incomplete.
                                  includes(line_items: [variant: [:images, :option_values, :product]]).
                                  find_or_initialize_by(guest_token: cookies.signed[:guest_token])
      associate_user
    end

    # Adds a new item to the order (creating a new order if none already exists)
    def populate
      order    = current_order(create_order_if_necessary: true)
      variant  = Spree::Variant.find(params[:variant_id])
      quantity = params[:quantity].to_i
      options  = params[:options] || {}

      # 2,147,483,647 is crazy. See issue #2695.
      if quantity.between?(1, 2_147_483_647)
        begin
          order.contents.add(variant, quantity, options)
        rescue ActiveRecord::RecordInvalid => e
          error = e.record.errors.full_messages.join(", ")
        end
      else
        error = Spree.t(:please_enter_reasonable_quantity)
      end

      if error
        flash[:error] = error
        redirect_back_or_default(spree.root_path)
      else
        respond_with(order) do |format|
          format.html { redirect_to cart_path }
        end
      end
    end

    def empty
      if @order = current_order
        @order.empty!
      end

      redirect_to spree.cart_path
    end

    def accurate_title
      if @order && @order.completed?
        Spree.t(:order_number, :number => @order.number)
      else
        Spree.t(:shopping_cart)
      end
    end

    def check_authorization
      order = Spree::Order.find_by_number(params[:id]) || current_order

      if order
        authorize! :edit, order, cookies.signed[:guest_token]
      else
        authorize! :create, Spree::Order
      end
    end

    private

      def order_params
        if params[:order]
          params[:order].permit(*permitted_order_attributes)
        else
          {}
        end
      end

      def assign_order_with_lock
        @order = current_order(lock: true)
        unless @order
          flash[:error] = Spree.t(:order_not_found)
          redirect_to root_path and return
        end
      end
  end
end

EDIT

It has become apparent since I posted this question that you do in fact need to declare your routes in a special way, despite rake routes showing them as correct.

In routes.rb, add this:

Spree::Core::Engine.routes.draw do
   # add your custom  routes here, e.g.
   get '/terms-and-conditions' => 'home#terms', as: :terms
end

This will then allow you to use the <%= link_to("Terms", terms_path) %> helper.

See Adding Routes to Rails' Spree E-Commerce for more details. I wish the documentation was better on this as it is mentioned NOWHERE apart from on SO as far as I can tell.

Upvotes: 3

Views: 5167

Answers (1)

Mohamad
Mohamad

Reputation: 35349

This happens because you are not passing an object to the form. So there's no id parameter in the route, and the router fails to make a match.

Your route is defined as a member action, which means it expects an id parameter. You are passing a symbol instead.

<%= form_for :order <-- problem

The clue is in the error message:

No route matches {:action=>"complete", :controller=>"spree/orders", :method=>:patch}

Notice how there's no id parameter in the hash in the error message?

To solve this, provide an object to the form. For example:

<%= form_for @order, url: complete_order_path(@order) do |f| %>

Where @order instance variable is set in the controller.

On a side note, you can define your routes like so:

resources :orders do
  member do
    patch :complete
  end
  # or, since it's only one route...
  patch :complete, on: :member
end

Notice you can use symbols, and you don't have to specify the controller because it's inferred from the resource name.

Finally, you don't need to tell the form that the method should be patch. Rails infers this from the object passed in, in this case @order. If it's new, the method will be POST, otherwise it will be PATCH.

Upvotes: 2

Related Questions