How to implement form objects - ruby-on-rails

Finally I was able to solve that issue
I try to implement a form object pattern in Ruby on Rails 4 and I just cannot find my mistake.
I constructed a sample application for a Phonebook, which includes the following models
Person
first_name
last_name
PhoneNumber
number
Email
address
[edit]
Thanks to #sockmonk I could solve one problem in this code. Still it is not working...
Here is the code I use
# app/models/person.rb
class Person < ActiveRecord::Base
has_many :phone_numbers
has_many :emails
end
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def new
#person_form = PersonForm.new
end
def create
#person_form = PersonForm.new(person_form_params)
#person_form.save
redirect_to person_path(#person)
end
def index
#people = Person.all
end
def show
#person= Person.find(params[:id])
end
private
def person_form_params
params.require(:person_form).permit(:person_first_name, :person_last_name, :phone_number_number, :email_address)
end
end
# app/forms/person_form.rb
class PersonForm
include Virtus
include ActiveModel::Model
# extend ActiveModel::Naming
# include ActiveModel::Conversion
# include ActiveModel::Validations
attr_reader :person
attr_reader :phone_number
attr_reader :email
attribute :person_first_name, String
attribute :person_last_name, String
attribute :phone_number_number, Integer
attribute :email_address, String
def persisted?
false
end
def save
if valid?
persist
true
else
false
end
end
private
def persist
#person = Person.create(first_name: person_first_name, last_name: person_last_name)
#phone_number = #person.phone_numbers.create(number: phone_number_number)
#email = #person.emails.create(address: email_address)
end
end
# app/views/people/new.html.erb
<h1>New Person</h1>
<%= form_for :person_form do |f| %>
<p>
<%= f.label :person_first_name %> </ br>
<%= f.text_field :person_first_name %>
<p>
<%= f.label :person_last_name %> </ br>
<%= f.text_field :person_last_name %>
<p>
<%= f.label :phone_number_number %> </ br>
<%= f.text_field :phone_number_number %>
<p>
<%= f.label :email_address %> </ br>
<%= f.text_field :email_address %>
<% end %>
(find the whole code at https://github.com/speendo/Phonebook2)
Btw. as you may have noticed, I use Virtus in this code - don't know if this is important.
If you need any additional information, please don't hesitate to let me know.

In your PersonForm class, you have these attributes:
attribute :person_first_name, String
attribute :person_last_name, String
But in your new.html.erb, you just reference :first_name and :last_name. You need to either change PersonForm to also use :first_name and :last_name, or change your template to use :person_first_name and :person_last_name.

The create method should redirect to #person_form.person instead.
try
def create
#person_form = PersonForm.new(person_form_params)
#person_form.save
redirect_to person_path(#person_form.person)
end

Related

How to validate multiple models in a single transaction?

In my rails (4.1.6) app, I have a contact model that has_one :address, :email
I construct a contact and related address and email in a single form using fields_for:
views/contacts/new.html.erb
<%= form_for #contact, ... %>
...
<%= fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
I want email to be required, while address is optional. In other words, if email is not provided, none of the 3 models should be created, but if only email is provided, the email and contact must be created.
One way that does work is to validate the params manually in the contacts_controller#create before constructing anything, and flash[:error] and return without saving if email is not specified, or save it if all is well:
contacts_controller.rb
def create
#contact = Contact.new
if(params_email_valid? params)
#contact.save!
#email = Email.create(...)
#email.save!
...
else
flash[:error] = 'Email must be specified to save a contact'
redirect_to :root
end
end
private:
def params_email_valid? params
!(params[:email][:display_name].blank? || params[:email][:mail_id].blank?)
end
Another way that may work is to drop down to SQL and validate everything through direct SQL calls in a transaction.
However, both of these are not 'the rails way', since validations belong in the models. So, I am trying to use some combination of validates_presence_of, validates_associated and custom validators to validate this scenario. The problem here is that model level validation of associated models requires either self to be already saved in the database, or the associated model to be already saved in the database. Is there a way to validate all these models in a single transaction?
Considering you have appropriate validations in the models:
class Contact <
has_many :addresses
has_many :emails
#add
accepts_nested_attributes_for :addresses, :emails #you can add some validations here to like reject_all if blank? see the docs
end
class Address <
belongs_to :contact
end
class Email <
belongs_to :contact
end
In your CompaniesController
def new
#contact = Contact.new
#contact.addresses.new
#contact.emails.new
end
def create
#contact = Contact.new(contact_params)
if #contact.save
#redirect add flash
else
#add flash
#render action: new
end
protected
def contact_params
#permit(#contact_fields, address_attributes: [#address_fields], email_attributes: [#email_fields])
end
And you would like to modify your form like this
<%= form_for #contact, ... do|f| %>
...
<%= f.fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= f.fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
So accepts_nested_attributes helps you validate the child as well as the parent and adds [child]_attributes getters and setters, So normally in your form what was contact[email][display_name] will become contact[email_attributes][display_name]

Rails 4 Nested Forms Simple_form_for & simple_field_for

I'm just trying to generate a simple nested form, like so:
<%= simple_form_for #profile do |f| %>
<%= f.input :first_name %>
<%= f.input :last_name %>
<%= f.input :phone_number %>
<%= f.simple_fields_for :addresses do |p| %>
<%= p.input :street %>
<%= p.input :city %>
<%= p.input :state, collection: us_states %>
<%= p.input :zip_code %>
<% end %>
<%= f.button :submit %>
My models:
class Profile < ActiveRecord::Base
belongs_to :customer
has_many :addresses
accepts_nested_attributes_for :addresses
end
class Address < ActiveRecord::Base
belongs_to :profile
end
My controller:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
end
end
Unfortunately that nested attribute addresses doesn't populate anything on the page, I would expect to see fields like street or city but I get nothing.
However, if I change <%= f.simple_fields_for :addresses do |p| %> to <%= f.simple_fields_for :address do |p| %> the fields display correctly.
Unfortunately doing this causes issues because I can't use the accepts_nested_attributes_for helper as outlined in the docs (as far as I can tell). Any idea why this isn't working?
The reason is because nested forms require created objects to work. It looks like Profile gets instantiated but Address does not.
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
#profile.addresses.create # this will create the address object that the nested form will use
end
end
I think you will need to create Profile as well rather than create an instance of it.
#profile = Profile.create
I've just been working with nested forms myself and this is how it worked for me.
The solution was to build the profile and the addresses in the #new action for it to work. Revised working code:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = current_customer.build_profile
#profile.addresses.build
end
end
You'll need to look at how your params come through, but since I have a has_many, they came through hashed with a key of a record id.

Defining an object in another model in Rails

I have migrated the :bank_name and :bank_account objects in User model.
I want two objects can be define from the Listings model in the listings/view to the User model columns.
I have already done (belongs_to, has_many)relations between two models.
But when I filled the bank_name and bank_account text_fields in Listing/view, I get the following error:
undefined method `bank_name' for #Listing:400123298
Here is my listing/view code:
<%= form_for(#listing, :html => { :multipart => true }) do |f| %>
...
<div class="form-group">
<%= f.label :bank_name %><br>
<%= f.text_field :bank_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :bank_account %><br>
<%= f.text_field :bank_account, class: "form-control" %>
</div>
</end>
listing/controller:
def new
#listing = Listing.new
end
def create
#listing = Listing.new(listing_params)
#listing.user_id = current_user.id
#listing.user_id = User.bank_name.build(params[:bank_name])
#listing.user_id = User.bank_account.build(params[:bank_account])
end
Several issues for you
Nested
As mentioned in the comments, what you're looking at is a nested model structure.
Simply, this means you'll be able to create an associative model from your "parent" - giving you the ability to define the attributes you need in your "parent" model, passing them through to the nested. This functionality is handled by accepts_nested_attributes_for in your parent model
The best resource you can use is this Railscast (only the start):
--
Fix
Here's how you can fix the problem:
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :bank_account
accepts_nested_attributes_for :bank_account
end
#app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
belongs_to :user
end
#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
def new
#listing = current_user.listings.new
#listing.user.build_bank_account
end
def create
#listing = Listing.new listing_params
#listing.save
end
private
def listing_params
params.require(:listing).permit(:listing, :params, user_attributes: [ bank_account_attributes: [] ])
end
end
This will help you do the following:
#app/views/listings/new.html.erb
<%= form_for #listing do |f| %>
...
<%= f.fields_for :user do |u| %>
<%= u.fields_for :bank_account do |b| %>
<%= b.text_field :name %>
<%= b.text_field :number %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
There is a slight twist to this tail, in that I'm not sure whether your passing of attributes through to your User model. This would be okay if the user was being created at the same time as your other attributes, but as it isn't, we may need to refactor the process of passing the nested data through
If this does not work, please comment & we can work to fix it!

Virtus: Replace accepts_nested_attributes (one-to-many) with a form object

Since more than a month I try to get behind the secrets of form objects in Rails 4.
Using virtus, I am already able to build very simple forms. However, I fail to develop a form object that replaces accepts_nested_attributes_for (in the model) and fields_for (in the form view).
In this question I explain a small phonebook-example: the form provides the possibility to enter a person's name and 3 phone numbers at once (find the whole code here).
Now I try to do the same with a form object. I get as far as this:
# forms/person_form_new.rb
class PersonFormNew
class PhoneFormNew
include Virtus
include ActiveModel::Model
attr_reader :phone
attribute :phone_number, String
end
include Virtus
include ActiveModel::Model
attr_reader :person
attribute :person_name, String
attribute :phone, PhoneFormNew
def persisted?
false
end
def save
if valid?
persist
true
else
false
end
end
private
def persist
#person = Person.create(name: person_name)
#person.phones.build(:phone)
end
end
# views/people/new.html.erb
<h1>New Person</h1>
<%= form_for #person_form, url: people_path do |f| %>
<p>
<%= f.label :person_name %> </ br>
<%= f.text_field :person_name %>
</p>
<p>
<%= f.fields_for :phone do |f_pho| %>
<%= f_pho.label :phone_number %> </ br>
<%= f_pho.text_field :phone_number %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
This gives me the error
undefined method `stringify_keys' for :phone:Symbol
line: #person.phones.build(:phone)
I fear however, this is not the only error.
Can you point me the way to realize a one-to-many assignment with a form object (preferable using Virtus)?
One solution is to create the associated object in a separate function on the form model. I was succussful by doing the following:
def persist!
#user.save!
#account.save!
create_admin_membership
end
def create_admin_membership
#membership = Membership.create! do |membership|
membership.user = #user
membership.account = #account
membership.admin = true
end
end
You can find an extended explanation here: http://w3facility.org/question/how-to-create-another-object-when-creating-a-devise-user-from-their-registration-form-in-rails/

Ruby on Rails: create records for multiple models with one form and one submit

I have a 3 models: quote, customer, and item. Each quote has one customer and one item. I would like to create a new quote, a new customer, and a new item in their respective tables when I press the submit button. I have looked at other questions and railscasts and either they don't work for my situation or I don't know how to implement them.
quote.rb
class Quote < ActiveRecord::Base
attr_accessible :quote_number
has_one :customer
has_one :item
end
customer.rb
class Customer < ActiveRecord::Base
attr_accessible :firstname, :lastname
#unsure of what to put here
#a customer can have multiple quotes, so would i use has_many or belongs_to?
belongs_to :quote
end
item.rb
class Item < ActiveRecord::Base
attr_accessible :name, :description
#also unsure about this
#each item can also be in multiple quotes
belongs_to :quote
quotes_controller.rb
class QuotesController < ApplicationController
def index
#quote = Quote.new
#customer = Customer.new
#item = item.new
end
def create
#quote = Quote.new(params[:quote])
#quote.save
#customer = Customer.new(params[:customer])
#customer.save
#item = Item.new(params[:item])
#item.save
end
end
items_controller.rb
class ItemsController < ApplicationController
def index
end
def new
#item = Item.new
end
def create
#item = Item.new(params[:item])
#item.save
end
end
customers_controller.rb
class CustomersController < ApplicationController
def index
end
def new
#customer = Customer.new
end
def create
#customer = Customer.new(params[:customer])
#customer.save
end
end
my form for quotes/new.html.erb
<%= form_for #quote do |f| %>
<%= f.fields_for #customer do |builder| %>
<%= label_tag :firstname %>
<%= builder.text_field :firstname %>
<%= label_tag :lastname %>
<%= builder.text_field :lastname %>
<% end %>
<%= f.fields_for #item do |builder| %>
<%= label_tag :name %>
<%= builder.text_field :name %>
<%= label_tag :description %>
<%= builder.text_field :description %>
<% end %>
<%= label_tag :quote_number %>
<%= f.text_field :quote_number %>
<%= f.submit %>
<% end %>
When I try submitting that I get an error:
Can't mass-assign protected attributes: item, customer
So to try and fix it I updated the attr_accessible in quote.rb to include :item, :customer but then I get this error:
Item(#) expected, got ActiveSupport::HashWithIndifferentAccess(#)
Any help would be greatly appreciated.
To submit a form and it's associated children you need to use accepts_nested_attributes_for
To do this, you need to declare it at the model for the controller you are going to use (in your case, it looks like the Quote Controller.
class Quote < ActiveRecord::Base
attr_accessible :quote_number
has_one :customer
has_one :item
accepts_nested_attributes_for :customers, :items
end
Also, you need to make sure you declare which attributes are accessible so you avoid other mass assignment errors.
If you want add info for diferent models i suggest to apply nested_model_form like this reference: http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast.
This solution is very simple and cleanest.

Resources