I'm new to Ruby and also Rails, and I'm trying to put together a nested form that ultimately creates a page but also allows the user to create individual parts inline before submitting the page. The inline form contains two buttons, one adds a part and the other one removes it. Here are my relevant files (please let me know if you need to see any other) and I'll list the problems I'm having after:
FYI, the gems I'm using are: Slim, Simple Form, cocoon and Bootstrap. On Rails 4.
_form.html.slim
= simple_form_for(#page, html: { class: 'form-horizontal' }) do |f|
= f.input :title, label: 'Title'
= f.input :description, label: 'Desc'
.form-group
.col-xs-10
label Parts
.form-inline
= f.simple_fields_for :parts do |part|
= render 'part_fields', f: part
.links
= link_to_add_association 'Add Part', f, :parts
= f.button :submit
_parts_fields.html.slim
= f.input :part_type, collection: ['String 1', 'String 2'], prompt: 'Part type', label: false
= f.input :part_title, placeholder: 'Part title', label: false
= f.input :part_desc, placeholder: 'Part description', label: false
= link_to_remove_association 'Remove Part', f
/models/page.rb
class Page < ActiveRecord::Base
belongs_to :project
has_many :parts
accepts_nested_attributes_for :parts
end
/models/part.rb
class Part < ActiveRecord::Base
belongs_to :page
end
/controllers/pages_controller.rb
class PagesController < ApplicationController
def index
#pages = Page.all
respond_to do |format|
format.html
format.json { render json: #pages }
end
end
def new
#page = Page.new
respond_to do |format|
format.html
format.json { render json: #page }
end
end
def edit
#page = Page.find(params['id'])
end
def create
#page = Page.new(page_params)
respond_to do |format|
if #page.save
format.html { redirect_to(#page) }
format.json { render json: #page, status: :created, location: #page }
else
format.html { render action: 'new' }
format.json { render json: #page.errors, status: :unprocessable_entity }
end
end
end
def update
#page = Page.find(params['id'])
respond_to do |format|
if #page.update_attributes(page_params)
format.html { redirect_to(action: 'index') }
format.json { head :ok }
else
format.html { render action: 'edit' }
format.json { render json: #page.errors, status: :unprocessable_entity }
end
end
end
def page_params
params.require(:page).permit(:title, :description, parts_attributes: [:id, :part_type, :part_title, :part_desc])
end
end # End Pages Controller
Routes
resources :projects do
resources :pages
end
resources :pages
resources :parts
Problems:
1) The form is not saving any data (can't edit/update current pages or create new ones). Update: fixed, see my own answer below.
2) On the partial I'm using a collection to have a dropdown menu, but those test values are hard-coded right now. How can I have a dropdown that populates each field with columns from the db?
3) The inline form elements which come from the partial are not being rendered back on the main form. All I see is the "Parts" label and no elements underneath it. Update: #pages.parts.build solved this.
Appreciate any help you guys can give me.
You have a #page object with no parts. So when it tries to render the fields_for, it does! Just, zero times. Add #page.parts.build in your controller to add one.
Not sure; too tired right now. Try changing save to save! and update_attributes to update! so Rails throws an error instead of failing silently. BTW, why do you have if params[:page] inside your page_params? That isn't needed; that's what require(:page) does.
ActiveRecord provides a column_names method, that returns an array of column names.
Actually, #page.parts.build will always build a new part, even if the user did not request this (it could be what you want). Now you only show the Add button for each nested part. I would assume you would want only one Add button, as follows:
.form-inline
= f.simple_fields_for :parts do |part|
= render 'parts_fields', f: part
= link_to_add_association 'Remove Part', f, :parts
= link_to_add_association 'Add Part', f, :parts
This will always show the Add Part link (and only once).
As to not saving, possible reasons are:
the data is not posted to the server (check your logfile, possible reasons are errors in your html, e.g. multiple identical ids)
the data is blocked by your strong-parameters command
the data is blocked by the reject_if condition of the accepts_nested_attributes_for
a failed validation
Now, if the code displayed is your actual code everything seems ok. So could you show us what is posted to the server? (check your logfile).
The form is now saving data as expected. In one of my partials I had two fields that were identical and thus creating a silent conflict for me. In addition, after I resolved this I started to get an Unpermitted parameter: _destroy error, and the fix was to add :_destroy to the list of required params for parts_attributes.
Related
I have two models: one for contacts ("Contatos") and one for users ("Usuarios"). Contatos has_one Usuario , as follows:
class Contato < ApplicationRecord
has_one :usuario, dependent: :destroy
accepts_nested_attributes_for :usuario,
allow_destroy: true
And
class Usuario < ApplicationRecord
has_secure_password
belongs_to :contato
validates_presence_of :login, :password
validates_uniqueness_of :login
end
I want to use one form for creating and editing both models. The _form partial that I currently have is this:
<%= form_with(model: contato, local: true) do |contato_form| %>
<%= if contato.errors.any?
showferr contato
end %>
#Here are the inputs for contato, I cut them out so it wouldn't be too long to read.
Bellow (same file as above) there is a check box for the Contato model that I left on, it sets a Boolean in the model(and DB) telling if the contact has a user on not, additionally I use some JavaScript (Coffee) to toggle the whole user (Usuario) form part based on the checkboxe's value .
<div class="form-group">
<%= contato_form.label :possui_usuario, :class => 'inline-checkbox' do %>
Possui usuário
<%= contato_form.check_box :possui_usuario, {id: "hasUser", checked: #contato.possui_usuario} %>
<% end %>
</div>
</div>
<div id="userPart" class="findMe" <% unless #contato.possui_usuario %> style="display:none;" <% end %> >
<h2> Usuário: </h2>
<div class="container">
<%= contato_form.fields_for :usuario, #contato.usuario do |usuario_form| %>
<%= render partial: 'usuarios/campos_usuario', locals: {form: usuario_form, object: #contato} %>
<% end %>
</div>
</div>
<br/>
<div class="container-fluid text-right">
<%= contato_form.submit 'Confirmar', :class => 'btn-lg btn-success' %>
</div>
<% end %>
The partial form for the Usuario model is rendering ok, but what I want to do is to only create and/or validate the user part if the checkbox is selected (if I say that the contact does have a user).
Here's what I attempted last (there were many attempts):
At Contato model:
attr_accessor(:has_user)
#has_user = 0
before_validation do |record|
#has_user = record.possui_usuario
end
def self.user?
#has_user == 1
end
validates_presence_of :nome
validates_length_of :nome, in: 1..45
validates_presence_of :email
validates_format_of :email, with: email_regex
validates_associated :usuario, if: user?
Controller for Contato:
class ContatosController < ApplicationController
before_action :set_contato, only: [:show, :edit, :update, :destroy]
# GET /contatos
# GET /contatos.json
def index
#contatos = Contato.all
#page_title = 'Contatos'
end
# GET /contatos/1
# GET /contatos/1.json
def show
#page_title = 'Ver contato: ' + #contato.nome
end
# GET /contatos/new
def new
#contato = Contato.new
#contato.build_usuario
#contato.ativo = true
#page_title = 'Novo contato'
end
# GET /contatos/1/edit
def edit
#page_title = 'Editar contato: ' + #contato.nome
unless #contato.possui_usuario
#contato.build_usuario
end
end
# POST /contatos
# POST /contatos.json
def create
#contato = Contato.new(contato_params)
respond_to do |format|
if #contato.save
flash[:notice] = 'Contato foi criado com sucesso.'
format.html {redirect_to #contato}
format.json {render :show, status: :created, location: #contato}
else
flash[:warn] = "Erro ao criar contato."
format.html {render :new}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# PATCH/PUT /contatos/1
# PATCH/PUT /contatos/1.json
def update
respond_to do |format|
if #contato.update(contato_params)
format.html {redirect_to #contato, notice: 'Contato foi atualizado com sucesso.'}
format.json {render :show, status: :ok, location: #contato}
else
format.html {render :edit}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# DELETE /contatos/1
# DELETE /contatos/1.json
def destroy
#contato.destroy
respond_to do |format|
format.html {redirect_to contatos_url, notice: 'Contato deletado com sucesso.'}
format.json {head :no_content}
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contato
#contato = Contato.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def contato_params
params.require(:contato).permit(:id, :empresa_id,
:ativo, :nome,
:cargo, :celular,
:email, :nascimento,
:observacoes, :mensagem_instantanea,
:tipo_msg_inst, :possui_usuario,
usuario_attributes: [:login, :password, :permissoes, :id, :contato_id, :_destroy])
end
end
Sorry for the long question and big code blocks.
I see two holes in the data presented currently ...
First, your controller action where create is called is where you should be testing to see if you are calling to the model / activerecord.
Something like ...
def create
if #contato && #contato.usuarios # might be able to just do last half
respond_to do |format|
if #contato = #contato.create!(contato_params) # note the bang or '!'
format.html { redirect_to #contato, notice: 'contato was successfully created.' }
else
format.html { render :new }
end
end
end
end
Without seeing your controller - I am going to guess you didn't nest your controller via Rails strong_param feature properly. Note here - these two won't run, I'm not quite sure what information is needed, but I wanted you to make sure if you are nesting your models and using a single controller - you are away you need to nest your models in strong_params (google search nested rails strong_params for thousands of help / hits).
params.require(:contato).permit(:login, :password, usuario: [id, ...] )
If that's not it - also tell us if all the functionality of create/read/update/destroy works normally & you are just looking to limit it to create in certain circumstances?
Update - based on the controller - just move your check for create from the model & move it to the controller at the start of the #create action ... maybe start with ...
def create
# Note - here you will have to inspect contato_params to find syntax
if contato_params[:usuario_attributes][:contato_id]
... rest of action wrapped in here ...
end
end
... once again ... you will need to work out exact syntax - but just like you did with the edit - this spot is where you control the creation - not in the model.
More specifically I see this #contato.possui_usuario in the form ... that's probably the variable you want to check against in your controller, but perhaps my suggestion is more important - I can't tell you that with certainty - I'm also not sure you need the has_user trick per say in model & might be tempted to do a controller version in the private method section ...
class ContatosController
private
def has_user?
... whatever ...
end
Clarification from comment:
If I move the control over the user form part to the controller (which
makes a lot of sense) how would I about canceling the
validates_associated part of the model in case the user decides that
this contact wont have any users?
You don't move the form control (defined as variable in the form), you move the model method that deals with the form control to the controller - then you can wrap it all in a transaction to rollback any other changes OR if you build your activerecord out with #build it will do it for you.
First steps with RoR, trying to wrap my head around basic concepts. Following excercise: I have pupils and schoolclasses, both Active Record entities with a many to many (has_and_belongs_to_many) to each other. Now I have a form to create a new pupil. On this form there is also a form.select to pick the class for the pupil, but I can´t get this to work, I can´t get the controller to create a new record for the join table.
Schoolclass.rb
class Schoolclass < ApplicationRecord
has_and_belongs_to_many :pupils
end
Pupil.rb
class Pupil < ApplicationRecord
has_and_belongs_to_many :schoolclasses
end
Relevant part of the _form.html.erb
<div class="field">
<%= form.label :schoolclass %>
<%= form.select(schoolclass.id, schoolclasses_for_select) %>
</div>
schoolclasses_for_select is just a helper for populating the select box
def schoolclasses_for_select
Schoolclass.all.collect{ |s| [s.name, s.schoolyear] }
end
Everything I have tried on the controller has failed miserably. Somehow, I mostly end up with the controller trying to pass the schoolclass (as a String) as an attribute to the new Pupil, or with a MethodNotFound error. In my understanding it should work something like this :
#klass = params[:schoolclass]
pupil.schoolclasses << #klass
but it doesn´t.
Thanks in advance for any help.
Edit1: the create code
def create
#pupil = Pupil.new(pupil_params)
respond_to do |format|
if #pupil.save
format.html { redirect_to #pupil, notice: 'Pupil was successfully created.' }
format.json { render :show, status: :created, location: #pupil }
else
format.html { render :new }
format.json { render json: #pupil.errors, status: :unprocessable_entity }
end
end
end
def pupil_params
params.require(:pupil).permit(:nachname, :vorname, :schoolclass)
end
That is the part that works. What I haven't managed is to find the correct Schoolclass record and pass it to the pupil.
Issues
First argument to your form.select should be the field name i.e. :schoolclass_id. You can still keep the label Schoolclass.
I believe you want id of schoolclass to be passed in params when selected. For that to happen, change your options for select to Schoolclass.all.collect{ |s| [s.name, s.id] }
Biggest, Your association says a pupil can have multiple schoolclasses but your form doesn't support it. Have you handled it some other way?
Fixes
So, do something like (this does not support multiple schoolclasses selection):
<%= form.select :schoolclass_id, Schoolclass.all.collect{ |s| [s.name, s.id] } %>
And in your controller
def create
#pupil = Pupil.new(pupil_params)
# Find schoolclass from `schoolclass_id` and associate it to `#pupil`
schoolclass = Schoolclass.find(params[:pupil][:schoolclass_id]) # Handle case when schoolclass not selected in form
#pupil.schoolclasses |= [schoolclass]
respond_to do |format|
...
end
end
private
def pupil_params
params.require(:pupil).permit(:nachname, :vorname)
end
I'm trying to create a list of items within a "Todo list", however, I'm not sure if I'm doing this correctly with nested attributes. I think using a nested attribute is the right attempt because there's going to be a large list of items, and it will be associated with the correct "Todo list" based on ids.
Example of what the tables might look like when records are populated
Todo table
id list
1 grocery shopping
2 health insurance
Item table
id todo_id name
1 1 buy milk
2 1 buy cereal
3 2 Blue Shield
4 2 Healthnet
5 1 buy cherries
Although, with my attempt below, my application is not saving any of the data into the Item database.
Todo Controller
class TodoController < ApplicationController
def new
#todo = Todo.new
#todo.items.build
end
end
Todo Model
class Todo < ActiveRecord::Base
belongs_to :user
has_many :items
accepts_nested_attributes_for :items
end
Item Model
class Item < ActiveRecord::Base
belongs_to :todo
end
Todo View
<%= simple_form_for(#todo) do |f| %>
<%= f.input :list %>
<%= f.simple_fields_for :items do |g| %>
<%= g.input :name %>
<% end%>
<%= f.button :submit %>
<% end %>
I was able to have the name field show up in my view, but when I save it, it doesn't save into the database, however, I'm able to save the list into the database, and then when I try to edit the record, the name field doesn't show up anymore to be able to edit.
EDIT: to show create method
This is my current Create Method in Todo Controller
def create
#todo = Todo.new(todo_params)
respond_to do |format|
if #todo.save
format.html { redirect_to #todo, notice: 'Todo was successfully created.' }
format.json { render :show, status: :created, location: #todo }
else
format.html { render :new }
format.json { render json: #todo.errors, status: :unprocessable_entity }
end
end
end
Not sure if Edit needs to have something, but I only have this from generating a scaffold of Todo
def edit
end
EDIT 2 show todo_params
def todo_params
params.require(:todo).permit(:user_id, :list)
end
You must add the nested params to your strong params
def todo_params
params.require(:todo).permit(:user_id, :list, items_attributes: [:id, :text, ...])
end
Note about todo_id :
You don't need to add :todo_id in items_attributes list, because you already have the TODO as context.
#todo = Todo.new(todo_params)
In the above code, your todo_params will contain some item_attributes linked to #todo. ie, it's similar to doing
#todo.items.build
It will already create an item with a todo_id corresponding to #todo.id
You need to add the items to the list of whitelisted attributes
def todo_params
params.require(:todo).permit(
:user_id,
:list,
items_attributes: [ # you're missing this
:id,
:name
]
)
end
Recently I added validations to one of the models in my application, and this seems to have caused a somewhat strange behaviour that I'm not handling properly in my code.
Here's a hypothetical example:
Clients
# name: string, phone: string, address: string
class Client < ActiveRecord::Base
has_many :transactions
accepts_nested_attributes_for :transaction, allow_destroy: true
validate :phone, numericality: true
end
Transactions
# p_date: date, location_id: integer
class Transaction < ActiveRecord::Base
belongs_to :client
end
This is how the controller would look like (again, have in mind that this is hypothetical):
PurchasesController
before_action :set_client, only: [:show, :edit, :update, :destroy]
def update
respond_to do |format|
if #client.update(client_params)
format.html { redirect_to clients_path, notice: 'Updated Succesfully' }
format.json { render :show, status: :ok, location: #client }
else
format.html { render :edit }
format.json { render json: #client.errors, status: :unprocessable_entity }
end
end
end
def client_params
params.require(:client).permit(
:name, :phone, :address,
transaction_attributes: [:id, :p_date, :location_id, :_destroy]
)
end
def set_client
#client = Client.find(params[:id])
end
For new records this works fine, but when I run into old ones that do not conform to the new validation rules in the phone number, the nested attributes aren't saved, because it's parent record is no longer valid.
I'm trying to find a way how to handle such errors.
Currently, this would be handled by the else condition in if #client.update(client_params) in the controller. When an error happens, the controller renders the :edit action, which results in another error in my view, cause now the helper that generates the fields for the nested form is receiving a null value for #client.
The view in question that generates the error looks like this:
purchases/:client_id/edit.html.haml
= form_for #client, :url => {:controller => 'purchase', :action => 'update'} do |f|
- if #client.errors.any?
#error_explanation
%h2
The following errors were found:
%ul
- #client.errors.full_messages.each do |message|
%li= message
=render 'form', f: f
.actions
=f.submit 'Save Changes', :class => 'btn btn-md btn-primary'
The error says: "First argument in form cannot contain nil or be empty", which I'm assuming it is cause the render is not sending the id of the Client.
In case you're wondering, I'm using form_for #client, :url => {:controller => 'purchase', :action => 'update'} do |f| cause this view is not in the Client controller. if I omit the extra parameters, the form is sent directly to the Client controller, which has different code pertaining only to the Client model.
I've partially managed to work around this by using the following in the update action:
def update
respond_to do |format|
if #client.update(client_params)
# *snip*
else
format.html { redirect_to edit_purchase_path(#client) }
format.json { render json: #client.errors, status: :unprocessable_entity }
end
end
end
This will redirect me back to the edit action, but no errors are printed. I get an identical page with the values used before I edited the values of the Transaction. I'm think there must be an easy way to send the errors back to the view, but I'm not sure where are these supposed to be used.
The "original" controller was generated by a scaffold, so the render :edit part is from the scaffolding itself. I'm aware my example could be somewhat vague (I'm just transcribing what I'm experiencing), so bear with me if this sounds a little odd. I'll gladly go into more detail if the information provided isn't enough.
I have an Order model.
Customers get a handful of consumer-friendly views that let them create and view their own orders, all backed by an Orders controller.
Admins get the full range of views to create, edit, view, delete and manage orders, backed by a Purchases controller.
As far as I can tell, the Purchases controller should only be speaking to the Order model, but the following error message makes me think it's looking for a non-existant Purchase model:
ActiveRecord::StatementInvalid in PurchasesController#new
NameError in PurchasesController#new
uninitialized constant Purchase
Rails.root: /Users/steven/Dropbox/testivate
Is that what the error means? If it does, how do I stop the Purchases controller from trying to find a Purchase model?
My code...
app/controllers/purchases_controller.rb:
class PurchasesController < ApplicationController
def new
#purchase = Order.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #purchase }
end
end
def create
#purchase = Order.new(params[:purchase])
respond_to do |format|
if #purchase.save
format.html { redirect_to purchase_path(#purchase), notice: 'Purchase was successfully created.' }
format.json { render json: #purchase, status: :created, location: #purchase }
else
format.html { render action: "new" }
format.json { render json: #purchase.errors, status: :unprocessable_entity }
end
end
end
end
/config/routes.rb:
Testivate::Application.routes.draw do
resources :orders
resources :purchases
end
/app/views/purchases/new.html.haml:
.textbox
%p#notice= notice
%h1 New Purchase
= render 'form'
= link_to 'List Purchases', purchases_path
/app/views/purchases/_form.html.haml:
= simple_form_for #purchase do |f|
= f.error_notification
= f.input :name
= f.button :submit
Update: so I've just realised that 'transaction' is a reserved word in Rails so I've changed that. But is there anything else I need to fix?
*Update 2: When I completely comment out the #new view and the _form, I still get the error, so I think the problem is in my controller or routes or somewhere other than with my use of simple_form.*
Update:
This is half of the answer. For the other half see the comments on the question.
Original answer:
Part of the problem here is that by default form_for (which simple_form_for is built atop) assumes certain things about what paths to use for a given record, which it derives from the class of that record. In your case, since #purchase is actually an instance of the Order class, those assumptions are going to screw things up for you.
If you look at the form generated by simple_form_for, you'll see that it gives the form both an id and a class of new_order, and that the fields in the form have names like order[name]. To get rails to use purchase instead of order in the form, you have to pass it an :as option.
From the docs for form_for:
If you have an object that needs to be represented as a different parameter, like a Person that acts as a Client:
And the example they give:
<%= form_for(#person, :as => :client) do |f| %>
...
<% end %>
In your case, you'd actually want to tell it to treat #purchase like a "purchase":
= simple_form_for #purchase, :as => :purchase do |f|
= f.error_notification
= f.input :name
= f.button :submit
That will get the form ids, etc. right, so that you can use e.g. params[:purchase] in your create action as you have above. But this is not enough, because the action (URL) for the form will still end up being /orders. To change that, add an :url option:
= simple_form_for #purchase, :as => :purchase, :url => purchases_path(#purchase) do |f|
= f.error_notification
= f.input :name
= f.button :submit
This will also solve the other question you posted.