The miracle of Rails (with a custom update action)

So, for the last couple of days I've been hammering at working out the following conundrum. How do I update an existing record one way through the "edit/update" RESTful way, but, edit the same record, exposing different fields, through a custom update action.

In my case I have a business with a certain number of gift certificates. From the index page listing gift certificates, I can Edit the gift certificate for price and comments, but nothing else. Other fields are:

active: true by default, false once redeemed

date_redeemed: blank until redeemed

remaining_amount: which is the full price until redeemed and the remainder once partially redeeemed or 0 when fully redeemed.

So, you see the word "redeem" right?

What I want is to have a redeem action in my gift_certificates_controller. I want that action, when called, to present a form that will not allow editing of the price, but will allow the ticket to be redeemed, which means having the amount outstanding set to 0 and the active flag set to false with the ability to leave a redemption comment (since comments are polymorphic).

I ran into a serious problem with this, but the general path appeared to be to route a link very specifically to a form. Such as this:

<%= form_for(@gift_certificate, url: url_for controller: 'gift_certificates', action: 'redeem') do |f| %>

The normal routing is:

<%= form_for([@customer, @gift_certificate]) do |f| %>

That normal routing allows the gift certificate to be edited through the customer/gift_certificate nesting and automatically uses the update method when "patch" is submitted or when the Update button is submitted on the form.

The method for directing a form to a specific controller action rendered my form perfectly. When def redeem; end was empty like that a test form showed existing data and would show the right parameters when submitted. But, when I added a copy of the update action from the gift_certificate_controller I ran into problems:

def redeem
  @gift_certificate.update(gift_certificate_params)
end

This would throw an error when I hit the redeem link:

ActionController::ParameterMissing: param not found: gift_certificate

Some investigation told me this was not passing the right parameters from the view. I had to change some things here as well. I set a new set method for gift_certificate since this was coming in from the business controller. I played around with finding a way to get the update method to accept it had params.

Nothing would work.

So, i thought there must be another way.

In Ruby there always is.

So, I made my form start with:

<%= form_for([@business, @gift_certificate]) do |f| %>    

My link still points me to the "redeem" action in the controller, which is now empty again like def redeem; end. Kind of like an edit for update. Once I hit submit, I want the update action to take it and run. But, the update action currently requires a customer and different things. This is coming in from the business directly.

My logic is a customer buys a gift certificate. But, the business owner won't want to have to go to the customer to find it and redeem it, though each customer does have an index of their gift certificate purchases. My logic is the business owner wants ALL purchased certificates available to look at, so this is where I'd want to redeem it.

This requires a few changes to my setup code in the controller:

before_action :get_customer_business_and_owner, except: [ :redeem, :update ]
before_action :set_gift_certificate, only: [ :show, :edit]
before_action :set_redeem_gift_certificate, only: [ :redeem ]

Normall the :get_customer_business_and_owner would apply to :update. As would :set_gift_certificate. But now I can't have that apply as before actions.

I changed my form submit button to read:

      <%= f.submit 'Redeem Gift Certificate' %>

Now, when that button is clicked, it is part of the params hash. Specifically, it is params[:commit].

Now I update my update action:

def update

  if params[:commit] == "Redeem Gift Certificate"
    set_redeem_gift_certificate
    respond_to do |format|
      if @gift_certificate.update(gift_certificate_params)

       format.html { redirect_to gift_certificates_business_path(@business), notice: "Gift Certificate Redeemed." }
      else
       format.html { render action: 'new' }
      end
    end
  else
    respond_to do |format|
      get_customer_business_and_owner
      set_gift_certificate
      if @gift_certificate.update(gift_certificate_params)

       format.html { redirect_to [@owner, @business], notice: "Gift Certificate successfully edited." }
      else
       format.html { render action: 'new' }
      end
    end      
  end
end

So if params[:commit] == "Redeem Gift Certificate" it calls set_redeem_gift_certificate, which has the parameters for that. And functions normally.

And when it doesn't see that commit, it runs a normal process but I added in the two before actions here to assure it all was right.

I'm sure this isn't quite complete, but it seems to work. Ruby is interesting.

Here are my methods:

  def set_gift_certificate
    @gift_certificate = @customer.gift_certificates.find(params[:id])
  end

  def set_redeem_gift_certificate
    @business = Business.find(params[:business_id])
    @gift_certificate = @business.gift_certificates.find(params[:id])
    @owner = @business.owner
    @customer = @gift_certificate.customer
  end


  def get_customer_business_and_owner
    @customer = Customer.find(params[:customer_id]) 
    @business = @customer.business
    @owner = @business.owner
  end

  def gift_certificate_params
    params.require(:gift_certificate).permit(:customer_id, :active, :prices_attributes => [:id, :amount], :comments_attributes => [:id, :comment])
  end

  def redeem_gift_certificate_params
    params.require(:gift_certificate).permit(:active)
  end

  def update_certificate_info
    @gift_certificate.update_attribute(:business_id, @business.id)
    certificate_number = @business.gift_certificates.count
    @gift_certificate.update_attribute(:certificate_number, certificate_number)
  end
Back
paperclip paperclip