Dengke Liu
Dengke Liu

Reputation: 141

How to implement update controller method

I am working on web app development using ruby on rails. I want to enable users to upload images for their favorite food. I have food_item model and a food_image model. In the food_item model:

has_many :food_images
has_many :food_portions #this is the work done by another teammate

I also define in the food_item controller:

def food_item_params
  params.require(:food_items).permit(:name, :category, :description, food_images_attributes: [:id, :food_item_id, :avatar]).tap do |whitelisted|
  whitelisted[:portion] = params[:food_items][:portion]
  whitelisted[:price] = params[:food_items][:price]
end

Upvotes: 4

Views: 20214

Answers (3)

Richard Peck
Richard Peck

Reputation: 76774

There are two issues:

  1. You need to save your updated object
  2. You should be doing this within the bounds of resources (although not essential)

Resourceful

The first step is to ensure you're using this with the correct routes etc.

You shouldn't have an add_images method in your controller - you could achieve what you need with edit/update:

#config/routes.rb
resources :food_items do
    resources :images #-> if necessary, this should be its own controller rather than adding needless methods to your other controller
end

You should use the following controller setup:

#app/models/food_item.rb
class FoodItem < ActiveRecord::Base
   accepts_nested_attributes_for :food_images
end

#app/controllers/food_items_controller.rb
class FoodItemsController < ApplicationController
   def edit
      @food_item = FoodItem.find params[:id]
      @food_item.food_images.build
   end

   def update
      @food_item = FootItem.find params[:id]
      respond_to do |format|
        if @food_item.update food_item_params
           ...
        end
      end
   end

   private

   def food_item_params
     params.require(:food_items).permit(:name, :category, :description, food_images_attributes: [:id, :food_item_id, :avatar]) #-> this is enough (no need to "whitelist")
   end
end

This will give you the ability to load the following:

#url.com/food_items/:id/edit
#app/views/food_items/edit.html.erb
<%= form_for @food_item do |f| %>
= form_for @food_item, html: { :multipart => true } do |f|
  = f.label :title
  = f.text_field :title
 = f.fields_for :food_images do |p|
     = p.label :avatar
     = p.file_field :avatar
.actions
   = f.submit
<% end %>

This will submit to the "update" method, which should save the required object for you.

You should only upload one file at a time

If you need to upload multiple files, you'll need to use a gem such as cocoon to add them. Rails is great but not magical -- it has to build a single object with each fields_for.

I can explain more about this if required.

--

To give you context on why you should be using the edit / update methods for this, you need to look up the resourceful principle for object orientated programming.

enter image description here

This is built on the "resources" principle put forward at the inception of HTTP -- a standardized set of technologies which allow browsers to send and retrieve data from servers.

In short, it means there are certain conventions you should abide by to keep your app extensible.

Because Ruby/Rails is object orientated, everything you do in a well-tailored application should revolve around objects. Like resources, these allow you to create a system which is both flexible and extensible if done properly.

Thus, with your code, you have to remember that you're trying to add an image to the food items object. You should therefore be editing the food items object, updating it with the extra image; which the above code will help you achieve.

Upvotes: 5

user3506853
user3506853

Reputation: 814

You need to change food_item_params method :

From :

def food_item_params
  params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end

To :

def food_item_params
  params.require(:food_item).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end

Also, you can improve your code :

Add accepts_nested_attributes_for :food_images in food_item model, then make changes in controller's method

def add_image
  @food_item = FoodItem.find(params[:id])
  respond_to do |format|
      if @food_item.update_attributes(food_item_params)
          format.html { redirect_to @food_item, notice: 'Images uploaded successfully' }
      else
         format.html { redirect_to add_image_food_item_path}
    end
  end
end

Upvotes: 0

x6iae
x6iae

Reputation: 4164

Like Badheka said: "why are you querying and just saving the @food_item?

Anyways, Notice that when you try to create food_images, you did:

food_item_params[:food_image]['avatar'].each do |a|
   @food_image = @food_item.food_images.create!(:avatar=>a)
end

You were looping through the avatar in food_image attributes of food_item_params

However, if you check your food_item_params, you have:

def food_item_params
  params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, :avatar])
end

With this, in all possibility, the structure of the data that this params is expecting will be something like:

{
  food_items: 
    {
      food_images_attributes:
        {
          id: "",
          food_item_id: "",
          avatar: ""
        }
    }
}

while what is being returned by your food_item_params will be as follow:

{
      food_images_attributes:
        {
          id: "",
          food_item_id: "",
          avatar: ""
        }
    }

There are a number of issues with this:

1) The error you are getting: param is missing or the value is empty: food_items this suggests that the param that is coming in is not in the format specified above. the parent attribute food_items is missing, so that is the first part of your code that you need to rectify from inside the view where you are sending the avatar, or conform your food_item_params method to meet the structure of what is coming in through the params.

2) There is no food_item_params[:food_image]['avatar'] to loop on, since thee is no food_image in your params. What you have is food_image_attributes, so your loop will be on food_item_params[:food_image_attributes][:avatar]

3) Looping through the food_item_params[:food_image_attributes][:avatar] suggests that it is an array. If so, then the way you are permitting it is wrong too. You will have to specify that it is an array from inside the food_item_params method as follow:

def food_item_params
  params.require(:food_items).permit(food_images_attributes: [:id, :food_item_id, avatar: []])
end

If however, as I suspect, what is coming through your param is as follow:

food_images_attributes:
        {
          id: "",
          food_item_id: "",
          avatar: []
        }

Then your food_item_params method should be as follows:

def food_item_params
  params.require(:food_images_attributes).permit(:id, :food_item_id, avatar: [])
end

I'll advise you check your params and paste what it looks like here.

And as a last word, check what you are sending from the view, and set your controller accordingly, or check what you want the controller to expect, and send same from the view.

Upvotes: 0

Related Questions