Rails 4 Strong Params has_many with JSON - ruby-on-rails

I'm attempting to pass json up on the client side and have rails take care of handling the object creation.
Here are my models:
class Order < ActiveRecord::Base
has_many :order_items, :autosave => true
belongs_to :menu_session
end
class OrderItem < ActiveRecord::Base
belongs_to :order
has_one :menu_item
end
Controller
class OrderController < ApplicationController
#POST /order/create
def create
#order = Order.new(order_params)
#order.save
end
private
def order_params
params.require(:order).permit(:comments, :menu_session_id, :order_items => [:menu_item_id])
end
end
The json data:
{'order': {'comments': 'none', 'menu_session_id': '9', 'order_items':[{'menu_item_id': '5'}, {'menu_item_id': '5'}]}};
The javascript
var data = {};
data.order = {'comments': 'none', 'menu_session_id': '9', 'order_items':[{'menu_item_id': '5'}, {'menu_item_id': '5'}]};
$.post('http://localhost:3000/order/create', orders, function(){}, 'json');
Finally, the error log:
Started POST "/order/create" for 127.0.0.1 at 2013-07-10 22:30:36 -0400
Processing by OrderController#create as JSON
Parameters: {"order"=>{"comments"=>"none", "menu_session_id"=>"9", "order_items"=>{"0"=>{"menu_item_id"=>"5"}, "1"=>{"menu_item_id"=>"5"}}}}
Completed 500 Internal Server Error in 52ms
ActiveRecord::AssociationTypeMismatch (OrderItem(#28109220) expected, got Array(#16050620)):
app/controllers/order_controller.rb:5:in `create'
Clearly, either my json is messed up or the ruby .permit is wrong. However, I've been playing with variations of this for a while now and cannot get it to work. The official documentation doesn't seem to venture into this, and every example I have found here deals with forms.
Anyone have any idea what is going on? I can't be the first to try this approach.
UPDATE:
Worked around it by making the following changes:
class OrderController < ApplicationController
#POST /order/create
def create
#order = Order.new(order_params)
order_items = order_item_params
order_items.each do |item|
#order.order_items << OrderItem.new(menu_item_id: item)
end
#order.save
end
private
def order_params
params.require(:order).permit(:comments, :menu_session_id)
end
def order_item_params
params.require(:order_items)
end
end
json: {"order":{"comments":"none","menu_session_id":"9"},"order_items":["5","5"]}
I don't think this would be the best way to do it, so I'm going to leave the question unanswered for now in hopes there is a best practice.

The workaround is not necessary in this case. ActiveRecord provides an automagic way of creating child elements directly through the params hash. In order to accomplish this, follow the steps bellow:
Configure Nested Attributes in the model
class Order < ActiveRecord::Base
# autosave is already enabled with accepts_nested_attributes_for
has_many :order_items
belongs_to :menu_session
accepts_nested_attributes_for :order_items
end
Include a *_attributes key in your JSON. In your case, change the order_items key to order_items_attributes
{'order': {'comments': 'none', 'menu_session_id': '9', 'order_items_attributes':[{'menu_item_id': '5'}, {'menu_item_id': '5'}]}};
In your controller, make permit accept your new key
def order_params
params.require(:order).permit(:comments, :menu_session_id, :order_items_attributes => [:menu_item_id])
end
There is some more awesomeness possible to accomplish with Nested Attributes. For further information, see ActiveRecord::NestedAttributes at Rails API

Related

Rails undefined method

Why is this undefined? Does it have something to do with the #current_user?
I'm trying to create tasks for my challenges. And the created task should get /achievements. However, I get a GET 500 error.
This is the error I get:
NoMethodError at /achievements
==============================
> undefined method `achievements' for #<User:0x00000105140dd8>
app/controllers/achievements_controller.rb, line 5
--------------------------------------------------
``` ruby
1 class AchievementsController < ApplicationController
2
3
4 def index
> 5 #achievements = #current_user.achievements
6 render :json => #achievements
7 end
8
9 def new 10 #achievement = Achievement.new
This is my code in my controller
class AchievementsController < ApplicationController
def index
#achievements = #current_user.achievements
render :json => #achievements
end
def new
#achievement = Achievement.new
render :json => #achievement
end
#create a new achievment and add it to the current user
#check then set the acheivments pub challenge id to the current pub challenge
def create
#achievement = Achievement.new achievement_params
#achievement.user = #current_user.id
#achievement.pub_challenge = params[:id]
if #achievement.save
# render :json => #achievement #{ status: 'ok'}
else
render :json => {:errors => #achievement.errors}
end
end
def show
#achievement = Achievement.find params[:id]
render :json => #achievement
end
def destroy
#achievement = Achievement.find params[:id]
#achievement.destroy
end
private
def achievement_params
params.require(:achievement).permit(:pub_challenges)
end
end
You are missing the has_many :achievements relation in your User model.
You'll need to create the ActiveRecord associations you require:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements
end
#app/models/achievement.rb
class Achievement < ActiveRecord::Base
belongs_to :user
end
This will give you the ability to call the achievements method on any User objects you have.
Error
The error you have is described as such:
undefined method `achievements' for #<User:0x00000105140dd8>
This basically means that you're trying to call an undefined method on a User object. Might sound simple, but really, most people don't understand it.
To explain properly, you have to remember that Rails, by virtue of being built on Ruby is object orientated. This means that everything you do in Rails should be structured around objects - which are defined in your Models:
This means that each time you call an object, you're actually above to invoke a series of "methods" which will give you the ability to either manipulate the object itself, or any of the associated functionality it has.
The problem you have is that your User object doesn't have the achievements method. Whilst you could simply do the following to fix the issue, because it's Rails, you'll need to populate the record with associative data:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements #-> what you need
def achievements
#this will also fix the error you see, although it's fundamentally incorrect
end
end
Something that helped me with this type of error was that the database table was missing the relevant column. Adding the required column to the database fixed the issue.

How can I capture an instance generically?

I'm using Rails 3.2.19 and Ruby 2.1.2. I've been googling around trying to figure this out, but perhaps I'm not searching for the right thing. Anyway, I'll try and be as concise as possible.
I have a few different models that all have a name attribute. In my views I want to somehow be able to access that name attribute regardless of the instance name passed into the view. Currently my various controllers create instances of their respective models. For instance:
class PagesController < ApplicationController
def show
#page = Page.find(params[:id])
respond_to do |format|
format.html
end
end
end
-
class ProductsController < ApplicationController
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html
end
end
end
While I understand I could simply re-name the instances something generic, I was wondering if there was some way of accessing any/all instances while maintaining unambiguous instance names.
Basically something like this:
page.html.haml
%h1= resources[0].name #equates to #page.name
%h2= #page.some_other_attribute
or
product.html.haml
%h1= resources[0].name #equates to #product.name
%h2= #product.price
Where in each of the above resources[0] would be either #page or #product
You will have to define a route with an additional resource_type parameter to a generic controller or otherwise just include the resource_type into the url query parameter
/resources/product/17
or
/resources/17?resource_type=product
This will allow you to do the following in the controller
class ResourcesController < ApplicationController
def show
#resource = find_resource(params)
respond_to do |format|
format.html
end
end
private
def find_resource(params)
resource_klass = {
product: Product,
page: Page
}[params[:resource_type]]
resource_klass.find(params[:id])
end
end
Another Option would be to introduce another ResourceType Entity and define a polymorphic :has_one :belongs_to association to the actual resource entity (product, page). Then always search for ResourceTypes and load the polymorphic resource entity
class ResourceType < ActiveRecord::Base
belongs_to :resource, polymorphic: true
end
class Product < ActiveRecord::Base
has_one :resource_type, as: :resource
end
class Page < ActiveRecord::Base
has_one :resource_type, as: :resource
end
product_resource_type = ResourceType.create(...)
product = Product.create(resource_type: product_resource_type)
page_resource_type = ResourceType.create(...)
page = Page.create(resource_type: page_resource_type)
ResourceType.find(product_resource_type.id).resource
=> product
ResourceType.find(page_resource_type.id).resource
=> page
I figured this out after discovering instance_variables and instance_variables_get
Those methods will return all instance variables being passed into the view. From there I discovered that the :#_assigns instance variable contained the instances that I was looking for. So I iterated over them to find if any had the name attribute.
- instance_variable_get(:#_assigns).each do |var|
- if var[1].respond_to?("name")
%h1= var[1].name
There is probably a better way of accomplishing this, so if anyone has any opinions, they are welcome.

ActiveRecord::RecordNotFound - in a descendant class' associated_controller#index

I am attempting to locate a parent object in a nested controller, so that I can associate the descendant resource with the parent like so:
# teams_controller.rb <snippet only>
def index
#university = Univeresity.find(params[:university_id])
#teams = #university.teams
end
When I call find(params[:university_id]) per the snippet above & in line 6 of teams_controller.rb, I receive ActiveRecord::RecordNotFound - Couldn't find University without an ID.
I'm not only interested in fixing this issue, but would also enjoy a better understanding of finding objects without having to enter a University.find(1) value, since I grant Admin the privilege of adding universities.
The Rails Guides say the following about the two kinds of parameters in a website:
3 Parameters
You will probably want to access data sent in by the user or other
parameters in your controller actions. There are two kinds of
parameters possible in a web application. The first are parameters
that are sent as part of the URL, called query string parameters. The
query string is everything after “?” in the URL. The second type of
parameter is usually referred to as POST data. This information
usually comes from an HTML form which has been filled in by the user.
It’s called POST data because it can only be sent as part of an HTTP
POST request. Rails does not make any distinction between query string
parameters and POST parameters, and both are available in the params
hash in your controller:
It continues a little further down, explaining that the params hash is an instance of HashWithIndifferentAccess, which allows usage of both symbols and strings interchangeably for the keys.
From what I read above, my understanding is that Rails recognizes both parameters (URL & POST) and stores them in the same hash (params).
Can I pass the params hash into a find method in any controller action, or just the create/update actions? I'd also be interested in finding a readable/viewable resource to understand the update_attributes method thats called in a controller's 'update' action.
Please overlook the commented out code, as I am actively searching for answers as well.
Thanks in advance.
Here are the associated files and server log.
Webrick
teams_controller.rb
class TeamsController < ApplicationController
# before_filter :get_university
# before_filter :get_team
def index
#university = University.find(params[:univeristy_id])
#teams = #university.teams
end
def new
#university = University.find(params[:university_id])
#team = #university.teams.build
end
def create
#university = University.find(params[:university_id])
#team = #university.teams.build(params[:team])
if #team.save
redirect_to [#university, #team], success: 'Team created!'
else
render :new, error: 'There was an error processing your team'
end
end
def show
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def edit
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def update
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
if #team.update_attributes(params[:team])
redirect_to([#university, #team], success: 'Team successfully updated')
else
render(:edit, error: 'There was an error updating your team')
end
end
def destroy
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
#team.destroy
redirect_to university_teams_path(#university)
end
private
def get_university
#university = University.find(params[:university_id]) # can't find object without id
end
def get_team
#team = #university.teams.find(params[:id])
end
end
team.rb
class Team < ActiveRecord::Base
attr_accessible :name, :sport_type, :university_id
has_many :home_events, foreign_key: :home_team_id, class_name: 'Event'
has_many :away_events, foreign_key: :away_team_id, class_name: 'Event'
has_many :medias, as: :mediable
belongs_to :university
validates_presence_of :name, :sport_type
# scope :by_university, ->(university_id) { where(team_id: team_id).order(name: name) }
# scope :find_team, -> { Team.find_by id: id }
# scope :by_sport_type, ->(sport_type) { Team.where(sport_type: sport_type) }
# scope :with_university, joins: :teams
# def self.by_university(university_id)
# University.where(id: 1)
# University.joins(:teams).where(teams: { name: name })
# end
def self.by_university
University.where(university_id: university_id).first
end
def self.university_join
University.joins(:teams)
end
def self.by_sport_type(sport_type)
where(sport_type: sport_type)
end
def self.baseball
by_sport_type('Baseball/Softball')
end
end
university.rb
class University < ActiveRecord::Base
attr_accessible :address, :city, :name, :state, :url, :zip
has_many :teams, dependent: :destroy
validates :zip, presence: true, format: { with: /\A\d{5}(-\d+)?\z/ },
length: { minimum: 5 }
validates_presence_of :name, :address, :city, :state, :url
scope :universities, -> { University.order(name: 'ASC') }
# scope :by_teams, ->(university_id) { Team.find_by_university_id(university_id) }
# scope :team_by_university, ->(team_id) { where(team_id: team_id).order(name: name)}
def sport_type
team.sport_type
end
end
views/teams/index.html.erb
Placed in gists for formatting reasons
rake routes output: (in a public gist)
enter link description here
rails console
You're not going to want to have both:
resources :universities #lose this one
resources :universities do
resources :teams
end
As for params... you have to give a param. So, when you go to http://localhost:3000/teams there are no params, by default. If you go to http://localhost:3000/teams/3 then params[:id] = 3 and this will pull up your third team.
Keep in mind the nomenclature of an index. The index action of Teams, is going to list all of the teams. All of them. There is no one University there, so what are you actually trying to find? If anything, you'd have, for your University controller:
def show
#university = University.find(params[:id])
#teams = #university.teams
end
so, the address bar will be showing http://localhost:3000/universities/23, right? params[:id] = 23, then you can find the teams associated with that university.

Rails update related model data in update method

I have such code:
def update
#oil = Oil.find(params[:id])
#product_types = ProductType.all
if #oil.update_attributes(params[:oil])
if #oil.other_products_cross_lists.update_attributes(:cross_value => #oil.model.to_s.gsub(/\s+/, "").upcase)
redirect_to admin_oils_path
end
else
render :layout => 'admin'
end
end
but when i run it i get:
undefined method `update_attributes' for #<ActiveRecord::Relation:0x007f7fb4cdc220>
and my other_products_cross_lists isn't updated... Also i try update_attribute and get the same error.
What i do wrong?
Also when i run my destroy method
def destroy
#oil = Oil.find(params[:id])
if #oil.destroy
if #oil.other_products_cross_lists.destroy
redirect_to admin_oils_path
end
else
render :layout => 'admin'
end
end
other_products_cross_lists didn't destroy...
How can i solve this problem?
model:
class Oil < ActiveRecord::Base
has_many :other_products_cross_lists, :foreign_key => 'main_id'
class OtherProductsCrossList < ActiveRecord::Base
belongs_to :oil
other_products_cross_lists is an association on your Oil model.
You cannot use update_attributes on an Array or ActiveRecord:Relation object.
What you should do is
#oil.other_products_cross_lists.each {|list| list.update_attributes(:cross_value => #oil.model.to_s.gsub(/\s+/, "").upcase)}
for destroying
you can use
#oil.other_products_cross_lists.delete_all
or
#oil.other_products_cross_lists.destroy_all
You should check out the difference between delete_all and destroy_all for clarity.
as the error says other_products_cross_lists is a relation (I assume your model oil has_many other_products_cross_lists).
update_attribute is a method of an instance of a model, not a method of a relation.
I don't really understand, what you want to do with you update_attribute, but if user nested_attributes, then
#oil.update_attributes(params[:oil])
takes care of updating the relation.
Also, if you define your relation beween Oil and OtherProducts as dependend: :destroy Rails handles the removel of dependent records.

accepts_nested_attributes_for alias?

In Topic model:
class Topic < ActiveRecord::Base
has_many :choices, :dependent => :destroy
accepts_nested_attributes_for :choices
attr_accessible :title, :choices
end
During a POST create, the params submitted is :choices, instead of :choices_attributes expected by Rails, and giving an error:
ActiveRecord::AssociationTypeMismatch (Choice(#70365943501680) expected,
got ActiveSupport::HashWithIndifferentAccess(#70365951899600)):
Is there a way to config accepts_nested_attributes_for to accept params passing as choices instead of choices_attributes in a JSON call?
Currently, I did the attributes creation in the controller (which seems not to be an elegant solution):
def create
choices = params[:topic].delete(:choices)
#topic = Topic.new(params[:topic])
if choices
choices.each do |choice|
#topic.choices.build(choice)
end
end
if #topic.save
render json: #topic, status: :created, location: #topic
else
render json: #topic.errors, status: :unprocessable_entity
end
end
This is an older question, but I just ran into the same problem. Is there any other way around this? It looks like that "_attributes" string is hardcoded in the nested_attributes.rb code (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L337).
Assigning "choices_attributes" to a property when submitting a form is fine, but what if it's being used for an API. In that case it just doesn't make sense.
Does anyone have a way around this or an alternative when passing JSON for an API?
Thanks.
UPDATE:
Well, since I haven't heard any updates on this I'm going to show how I'm getting around this right now. Being new to Rails, I'm open to suggestions, but this is the only way I can figure it out at the moment.
I created an adjust_for_nested_attributes method in my API base_controller.rb
def adjust_for_nested_attributes(attrs)
Array(attrs).each do |param|
if params[param].present?
params["#{param}_attributes"] = params[param]
params.delete(param)
end
end
end
This method basically converts any attributes that are passed in to #{attr}_attributes so that it works with accepts_nested_attributes_for.
Then in each controller that needs this functionality I added a before_action like so
before_action only: [:create] do
adjust_for_nested_attributes(:choices)
end
Right now I'm only worried about creation, but if you needed it for update you could add that into the 'only' clause of the before_action.
You can create method choices= in model as
def choices=(params)
self.choices_attributes = params
end
But you'll break your setter for choices association.
The best way is to modify your form to return choices_attributes instead choices
# Adds support for creating choices associations via `choices=value`
# This is in addition to `choices_attributes=value` method provided by
# `accepts_nested_attributes_for :choices`
def choices=(value)
value.is_a?(Array) && value.first.is_a?(Hash) ? (self.choices_attributes = value) : super
end

Resources