Rails - Create an object in database from nested params attributes - ruby-on-rails

I have a rails application where the user creates a new job, and on the job/new _form i am having the user enter the information for the Company that the Job belongs too.
A job in the database is simply the Date it was created, the User who created it, and a additional foreign keys to tables like Company, Contact, Jobsite, etc.
A Company has_many jobs and a Job belongs_to a Company in my application.
So, before I can persist the Job into the database, I must first save the new Company so that I can then grab its primary key and put that in the Jobs company_id foreign key tuple.
I have everything coming back properly in the params hash like so:
>> params
=> {"utf8"=>"✓", "authenticity_token"=>"NiQO7Wlq87h3+YM//yIEMnctVKectfmZBb74suFOCcmg7g4YyCmGo2OiciOd3VuRDR52tKoE0v9nq1LYoTqHOQ==", "job"=>{"date"=>"2016-09-04 16:06:49 -0700", "user_id"=>"1", "company_id"=>"", "subcontractor_id"=>"", "jobsite_id"=>"", "companies"=>{"name"=>"Test Company", "phone"=>"(530)111-2222", "street"=>"3177 testing st", "city"=>"cameron park", "state"=>"ca", "zip"=>"95682", "email"=>"testemail#mail.com", "website"=>"testcompany.com"}}, "commit"=>"Create Job", "controller"=>"jobs", "action"=>"create"}
>>
And i can access the company information with params[:job][:companies]
So my theory is, in the Jobs controller, in def create, before i call #job.save, I must first do a #company.new(company_params) and #company.save so that I can save the company and then grab its primary key from the database.
But I need a little help with this.
Does anyone have some tips to give?
Edit:
This is a sample from my _form,
<%= f.fields_for #company do |company| %>
<div class="field">
<%= company.label :name %>
<%= company.text_field :name %>
</div>
Inside the Job Controller I have,
def new
#job = Job.new
#company = Company.new
end
def create
#job = Job.new(job_params)
#company = Company.new(params[:job][:company])
Job.rb has
accepts_nested_attributes_for :company
And the params are:
def job_params
params.require(:job).permit(:date, :user_id, :company_id, :subcontractor_id, :jobsite_id, :company_attributes => [:name, :phone, :street, :city, :state, :zip, :email, :website])
end
When i submit my form, the attributes come back as company instead of company_attributes, so it is telling me "Unpermitted parameter: company".
The way i have it set up, #job has not function to do #job.company.build, they are not nested that way. Company is its own table and a company can have many jobs, and all a job can do is perform #job.company
Which is why i need to create the company first so i can get its primary key and then say #job.company_id = #company.id
I hope this makes things more clear.

You could use nested_attributes_for :company or companies, looking at your code and seeing company_id, I'm assuming it's company.
=>{
...
"job"=>{
"date"=>"2016-09-04 16:06:49 -0700",
"user_id"=>"1", # <= Really shouldn't do this...
"company_attributes"=>{
"name"=>"Test Company",
"phone"=>"(530)111-2222",
"street"=>"3177 testing st",
"city"=>"cameron park",
"state"=>"ca", "zip"=>"95682",
"email"=>"testemail#mail.com",
"website"=>"testcompany.com"
}
},
...
}
Then in your permitted params you can do something like:
def job_params
params.require(:job).permit(:date, :company_id, :subcontractor_id, :company_attributes => [:name, :phone, :street, :city, :state, :email, :website])
end
You can then use fields_for :company in your form_tag. Also, add a blank company to the new job action.
def new
#job = Job.new
#job.build_company
end
EDIT
You can't use #company on feilds_for like this.
Perhaps fields_for :company, #company, but not just #company.
<%= f.fields_for :company do |company| %>
You have to use a symbol to represent the record's name.
Otherwise the fields name will be "job[company]" or in your case "job[companies]" instead of "job[company_attributes]".
And either way, you can just add the new objects to the new job's association instead of adding an instance variable.
#job.build_company
You can try it out yourself with this working repo: https://www.github.com/frank184/surveysays
And for reference:
http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for

Related

When to use attr:accessors in place of a permanent column in rails?

I'm creating my own website using Ruby on Rails. One thing that I've failed to comprehend is why and when to use attr:accessors in place of a permanent column for a model. For instance, let's say that I created a 'posts' model which would have a title, description and some content associated with it. Now should I do rails g model Post title:string description:text content:text or should I declare them as attr:accessible :title, :description, :content.
I'm not very experienced in rails, so please bear with me if this sounds too silly to you.
You can use attr_accessor if you need virtual attributes in model.
For eg: In contact us form you need not to see form data, but you need to send that data using email. So you can create attr_accessor for adding virtual attributes and can also apply validations on that.
class Contact
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
ref
attr_accessible is to white list of attributes that can be mass assigned in model.
class Comment < ActiveRecord::Base
attr_accessible :user_id, :content
end
def create
#so here params[:comment], have all parameters. But if those params are not in attr_accessible list it will not save it.
# you can try that by remove attr_accessible from model
#comment = Comment.new(params[:comment])
if #comment.save
flash[:notice] = "Successfully created comment."
redirect_to #comment
else
render :action => 'new'
end
end
Comment Form:
<%= form_for #comment do |f| %>
<%= f.error_messages %>
<%= f.hidden_field :user_id, value: current_user.id %>
<p>
<%= f.label :content %><br />
<%= f.text_area :content %>
</p>
<p><%= f.submit %></p>
<% end %>
Happy Coding...
To add to Pardeep's epic answer, you'll want to look at this RailsCast (RE "virtual attributes"):
attr_accessor basically creates a setter & getter method in the model.
Probably doesn't make any sense; what you have to remember is that each Rails model is a class. Classes form the backbone of object-orientated programming.
Since Ruby is object orientated, each time you do anything with the language, it expects classes to be invoked & manipulated. The basis of OOP is to load classes into memory & play with them; good write-up here.
In classic OOP, your classes would be hard-coded with a series of attributes:
class Mario
def jump
pos_y + 5
end
def pos_y
# gets y val from the viewport
end
end
This will allow you to send instructions to the program, in turn modifying the class:
#mario.jump
... this should modify the viewport etc in the way you defined within the class.
--
Rails is very similar to the above, except most of the attributes are defined by ActiveRecord;
#app/models/mario.rb
class Mario < ActiveRecord::Base
# attributes from db include height, weight, color etc
end
Rails models allow you to call:
#mario = Mario.find x
#mario.height = "255"
... however, they don't allow you to create attributes which are stored in memory only.
For example...
#app/models/mario.rb
class Mario < ActiveRecord::Base
attr_accessor :grown
end
The above will give you an instance value of grown, which will allow you to populate this independently of the database.
So say you wanted to...
#mario = Mario.find x
#mario.grown = true if params[:grown]
#mario.height += "150" if #mario.grown
Regarding the difference between attr_accessor and attr_accessible, you'll want to look up Rails 3 and mass assignment.
I came into Rails ~ 4.0, so I didn't have to deal with attr_accessible so much; it was basically the way to permit parameters in Rails 3 models.
In Rails 4/5, you use strong params in the controller:
#app/controllers/mario_controller.rb
class MarioController < ApplicationController
def create
#mario = Mario.new mario_params
#mario.save
end
private
def mario_params
params.require(:mario).permit(:x, :y, :z)
end
end

Ruby on Rails object saves with blank values

group controller:
def show
#cat = Category.find_by_id(params[:id])
if #cat.group
#group = #cat.group
#members = #cat.group.group_members.all
#mem = #group.group_members.build
else
#cat.build_group
#cat.save
#mem = #cat.group.group_members.build
end
end
def add_member
#cat = Category.find_by_id(params[:id])
#group = #cat.group
#group.group_members.build(member_params)
if #group.save
redirect_to group_path
end
view:
- if #members.length > 0
- #members.each do |member|
%ul
%li
= member.first_name
= simple_form_for #mem, url: member_add_path(#cat.id), html: {:id => 'step_two_form'} do |f|
= f.label "First name"
= f.input :first_name, label: false
= f.label "Last name"
= f.input :last_name, label: false
= f.label "Email"
= f.input :email, label: false
= f.label "Phone number"
= f.input :telephone, label: false
= f.button :button, "Add member"
When I submit this form I can see that a new object is created as there is a new <li> in the source however the object has blank values, regardless of the input.
params (in the group controller):
def member_params
params.require(:group_member).permit(group_members_attributes: [:first_name, :last_name, :email, :telephone, :relationship, :status])
end
In the terminal I can see that the values I input are being passed but for some reason are not being saved. Here is the terminal output:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"7odxnZzHoyjgF/oougDIVKNR/9RkZOlK3IOpCaUVvpQ=", "group_member"=>{"first_name"=>"name", "last_name"=>"name", "email"=>"name#name.com", "telephone"=>"1234567890"}, "button"=>"", "id"=>"22"}
All help is appreciated, thank you.
EDIT:
group_member.rb:
class GroupMember < ActiveRecord::Base
belongs_to :group
attr_accessor :first_name, :last_name, :email, :telephone, :relationship
end
Your member_params method doesn't need to specify group_members_attributes as a nested hash, you should just be able to permit the attributes directly (they'll be permitted on whatever you put in the require(...) part):
def member_params
params.require(:group_member).permit(:first_name, :last_name, :email, :telephone, :relationship, :status)
end
Extending #DylanMarkow's answer, if you are trying to save first_name, last_name, email, telephone, relationship fields in database then you need to remove the following line from GroupMember model:
attr_accessor :first_name, :last_name, :email, :telephone, :relationship
Due to the attr_accessor, the above mentioned fields are considered as virtual attributes and hence not saved in database.
UPDATE
Can you briefly explain what the purpose of attr_accessor is? I
thought it creates a getter and setter methods for the listed
attributes?
Yes, attr_accessor is a Ruby method and it creates getter and setter methods for an attribute. When you use attr_accessor within a Rails model, the listed attributes are treated as virtual attributes i.e., there values would be in memory/ accessible only till the instance of the model lives because it is not stored in the database fields (as it is marked as virtual attribute). In a Rails model you don't need to worry about getters and setters of attributes as ActiveRecord would take care of that.
combined with #DylanMarkows answer you are saving the group but not necessarily the member
def add_member
#cat = Category.find_by_id(params[:id])
#group = #cat.group
#group_member = #group.group_members.build(member_params)
if #group_member.save
redirect_to group_path
else
render ...
end
end
that might help also your method is missing an end and probably an else on save

Why does my update action work, but not the create action? Rails 4

cIn my Rails 4 app, my update action is working, so I can edit, but my create action is not. I can't figure out why I can add products_id and user_id in the edit form, but when I try to use create a new catalogue I get errors.
My create action:
def create
#catalogue = Catalogue.new(catalogue_params)
if #catalogue.save
redirect_to #catalogue, notice: "Catalogue was successfully created."
else
render action: "new"
end
end
My update action:
def update
if #catalogue.update(catalogue_params)
redirect_to #catalogue, notice: "Catalogue was successfully updated."
else
render action: "edit"
end
end
My strong parameters:
private
def set_catalogue
#catalogue = Catalogue.find(params[:id])
end
def catalogue_params
params.require(:catalogue).permit(:title, :url, :google_name, product_ids: [], user_ids: [])
end
From the form:
Forgot my form code:
.field
= f.collection_select(:user_ids, #catalogue.get_all_users, :id, :name, {}, multiple: true)
.field
= f.label :product_ids
= f.collection_select(:product_ids, #catalogue.get_all_products, :id, :title, {}, multiple: true)
.actions
= f.submit "Save"
The fields that are not working with create are the arrays: product_ids: [] and user_ids: []. If I click on one of the catalogues that I had created with a seed file, I can open it for edit and add a product or user from the drop down fields in the form. The catalogue is updated, no problem. The form has select and sends products and users as arrays.
But if I click on New Catalogue, I get this error:
2 errors prohibited this catalogue from being saved:
Catalogue users is invalid
Catalogue products is invalid
--- !ruby/hash:ActionController::Parameters
utf8: "✓"
authenticity_token: ddJsIAweWHoblmbOAjoNpZ0iPwi8ookrgH6HOzd/jh4=
catalogue: !ruby/hash:ActionController::Parameters
title: Testing Create
url: www.something.com
google_name: ''
user_ids:
- ''
- '1'
product_ids:
- ''
- '1'
commit: Save
action: create
controller: catalogue
I can see that the selections are there for each item but not the names, so I don't understand why it's not creating, and why if I open a previously created catalogue, I can add products and users via the form. But not create.
And this from the console:
Processing by CatalogueController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ddJsIAweWHoblmbOAjoNpZ0iPwi8ookrgH6HOzd/jh4=", "catalogue"=>{"title"=>"Testing Create", "url"=>"www.something.com", "google_name"=>"", "user_ids"=>["", "1"], "product_ids"=>["", "1"]}, "commit"=>"Save"}
I am using join tables between user and catalogue, and product and catalogue:
class CatalogueProduct < ActiveRecord::Base
belongs_to :catalogue
belongs_to :product
validates :product_id, :catalogue_id, presence: true
end
class Catalogue < ActiveRecord::Base
include ModelHelper
has_many :catalogue_users, dependent: :destroy
has_many :users, through: :catalogue_users
has_many :catalogue_products, dependent: :destroy
has_many :products, through: :catalogue_products
The user model follows the same patter as above.
I'm wondering if I need to convert those arrays to strings? Because in the above it's listing the actual id. But why then doesn't the update action need the conversion? I'm confused about what's going on and how to fix it. I kept getting errors when I tried converting to strings in the strong parameters.
Thank you!!!!!
Here's the problem:
2 errors prohibited this catalogue from being saved:
Catalogue users is invalid
Catalogue products is invalid
Your code looks good
--
Associative Data
I think the problem is you're trying to pass multiple ids for user_ids and product_ids. Issue being this only works if you've got join models for either of those items (so Rails can populate their collection objects).
Without knowing your model associations, I can only speculate, but I would surmise changing your user_ids & product_ids to user_id & product_id should solve the issue (considering you have the columns user_id and product_id in your catalogues table)
.field
= f.collection_select(:user_id, #catalogue.get_all_users, :id, :name, {}, multiple: true)
.field
= f.label :product_ids
= f.collection_select(:product_id, #catalogue.get_all_products, :id, :title, {}, multiple: true)
.actions
= f.submit "Save"
#app/controllers/home_controller.rb
def catalogue_params
params.require(:catalogue).permit(:title, :url, :google_name, product_id, user_id)
end
--
Models
Thinking about your system, I would suggest you have a deeper issue with the structure of your models. More specifically, you want to associate a catalogue with multiple products & multiple users?
If this is the case, I would structure the models as thus:
#app/models/catalogue.rb
has_and_belongs_to_many :products
has_and_belongs_to_many :users
#Tables:
#catalogues_users
#cataglogues_products
This will allow you to submit user_ids and product_ids to the Catalogue model
In create action, before #catalogue=Catalogue.new(catalogue_params), just add this snippet:
params[:catalogue][:user_ids].delete('')
params[:catalogue][:product_ids].delete('')
That's because your form is submitting an empty id for each associated model (users, products), which is of course invalid when trying to associate.

How to work with protected model attributes--foreign keys (mass-assignment protection error)

In my application I have a course model that belongs to three other models: user, subject, and student_level (they include has many in model descriptions).
To be able to create a course, I declared foreign keys for two models in course model as attr_accessible.
class Course < ActiveRecord::Base
attr_accessible :objectives, :title, :subject_id, :student_level_id
belongs_to :user
belongs_to :subject
belongs_to :student_level
This is my _fields.html.slim file for creating a course:
= render 'shared/error_messages', object: f.object
= f.label :title, t("activerecord.attributes.course.title")
= f.text_field :title
= f.label :subject_id, t("activerecord.attributes.subject.title")
= f.collection_select(:subject_id, Subject.all, :id, :title_for_select)
= f.label :student_level_id, t("activerecord.attributes.student_level.title")
= f.collection_select(:student_level_id, StudentLevel.all, :id, :title_for_select)
= f.label :objectives, t("activerecord.attributes.course.objectives")
= f.text_area :objectives, rows: 15, cols: 10
And this is my new method in courses_controller.rb
#GET /courses/new
def new
#course = current_user.courses.new
# #subjects = Subject.all
# #student_levels = StudentLevel.all
end
Above code shows that I am mass-assigning subject and student level attributes.
What is bothering me, is that in Hartl's Ruby on Rails Tutorial for version 3.2 (for example, p. 536, listing 10.7) these foreign keys should be protected. And there is an example for protected foreign key assignment.
Right now everything works fine. Also, my config/application.rb contains
config.active_record.whitelist_attributes = true
Right now if I remove subject_id and student_level_id from attr_accessible (so they become protected), the application gives
ActiveModel::MassAssignmentSecurity::Error in CoursesController#create
Can't mass-assign protected attributes: subject_id, student_level_id
My question: what is the best practice when creating an entity with two foreign keys, is there a way to create/edit without exposing foreign keys as attr_accessible for mass-assignment?
Thank you very much!
UPDATE:
#POST /courses/
def create
#course = current_user.courses.new(params[:course])
if #course.save
flash[:success] = t("messages.course_created")
redirect_to #course
else
render 'new'
end
end
By looking at the code of your course form my _fields.html.slim, it looks like you are taking subject_id, student_level_id from user only through select box, so they should be accessible.
What should be protected is : say for example, you have Institue model and course belongs to an Institue and you have a foreign_key as institute_id, then this institute_id should be protected.
Other Example which I can give you is, say
Project belongs_to User and User has many projects
and you have a foreign key user_id in projects table :
then while creating a project you should do something like :
#This will create a new project object with user_id set as id of current_user
current_user.projects.new
Now to create a project object with params :
current_user.projects.new(params[:project])
I am not sure that, their is any way to create a object with two protected attributes but definitely in your case the attributes subject_id, student_level_id should not be protected.

Can't mass-assign protected attributes for creating a has_many nested model with Devise

I've watched the RailsCast, another nested attributes video, lots of SO posts, and fought with this for a while, but I still can't figure it out. I hope it's something tiny.
I have two models, User (created by Devise), and Locker (aka, a product wishlist), and I'm trying to create a Locker for a User when they sign up. My login form has a field for the name of their new Locker (aptly called :name) that I'm trying to assign to the locker that gets created upon new user registration. All I'm ever greeted with is:
WARNING: Can't mass-assign protected attributes: locker
I've tried every combination of accepts_nested_attributes and attr_accesible in both of my models, yet still nothing works. I can see from the logs that it's being processed by the Devise#create method, and I know Devise isn't smart enough to create my models how I want :)
Here's the relevant bits of my two models:
# user.rb
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :locker_attributes
# Associations
has_many :lockers
has_many :lockups, :through => :lockers
# Model nesting access
accepts_nested_attributes_for :lockers
end
and
# locker.rb
class Locker < ActiveRecord::Base
belongs_to :user
has_many :lockups
has_many :products, :through => :lockups
attr_accessible :name, :description
end
# lockers_controller.rb (create)
#locker = current_user.lockers.build(params[:locker])
#locker.save
I'm assuming I need to override Devise's create method to somehow get this to work, but I'm quite new to rails and am getting used to the black box "magic" nature of it all.
If anyone can help me out, I'd be incredibly thankful. Already spent too much time on this as it is :)
EDIT: I realized I omitted something in my problem. My Locker model has three attributes - name, description (not mandatory), and user_id to link it back to the User. My signup form only requires the name, so I'm not looping through all the attributes in my nested form. Could that have something to do with my issue too?
EDIT 2: I also figured out how to override Devise's RegistrationsController#create method, I just don't know what to put there. Devise's whole resource thing doesn't make sense to me, and browsing their source code for the RegistrationsController didn't help me much either.
And for bonus points: When a user submits the login form with invalid data, the Locker field always comes back blank, while the regular Devise fields, username & email, are filled in. Could this also be fixed easily? If so, how?
first, you have a typo :
attr_accessible :locker_attributes
should be plural :
attr_accessible :lockers_attributes
then, the standard way to use nested_attributes is :
<%= form_for #user do |f| %>
<%# fields_for will iterate over all user.lockers and
build fields for each one of them using the block below,
with html name attributes like user[lockers_attributes][0][name].
it will also generate a hidden field user[lockers_attributes][0][id]
if the locker is already persisted, which allows nested_attributes
to know if the locker already exists of if it must create a new one
%>
<% f.fields_for :lockers do |locker_fields| %>
<%= locker_fields.label :name %>
<%= locker_fields.text_field :name %>
<% end %>
<% end %>
and in you controller :
def new
#user = User.new
#user.lockers.build
end
def create
# no need to use build here : params[:user] contains a
# :lockers_attributes key, which has an array of lockers attributes as value ;
# it gets assigned to the user using user.lockers_attributes=,
# a method created by nested_attributes
#user = User.new( params[:user] )
end
as a side note, you can avoid building a new locker for new users in controller in different ways:
create a factory method on User, or override new, or use an after_initialize callback to ensure every new user instantiated gets a locker builded automatically
pass a specific object to fields_for :
<% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>
Someone helped me figure out the solution in a more "Rails 4'y" way with strong attributes & how to override Devise's sign_up_params (to catch all the data coming from my signup form).
def sign_up_params
params.require(:user).permit(:username, :email, :password, :lockers_attributes)
end
Gemfile addition: gem 'strong_parameters'
Commenting out the attr_accessible statement in my user.rb file, since apparently strong parameters eliminate the need for attr_accessible declarations.
# attr_accessible :username, :email, :password, :password_confirmation, :lockers
And the/a correct way of building a Locker before submitting the form: at the beginning of the nested form:
<%= l.input :name, :required => true, label: "Locker name", :placeholder => "Name your first locker" %>
Thanks again for all your help. I know a question like this is difficult to answer without seeing the whole codebase.

Resources