How do you create a nested object form in Rails? - ruby-on-rails

I'm building a simple Rails-based travel app for a school project.
I want to make a form that enables the user to enter data such as the name of their Trip, two destinations, and each destination will have a location.
How can I do this with a triple nest form?
I'm very confused by the build method right now. Below is the def new method and the private methods in the controller.
class TripsController < ApplicationController
def new
#trip = Trip.new
Destination.all.each do |destination|
#trip.destinations.build.location
end
private
def set_trip
#trip = Trip.find(params[:id])
end
def trip_params
params.require(:trip).permit(:name, :start_date, :end_date,
:destination_1, :destination_2, :user_id, :destinations_attributes => [:name,
:locations_attributes => [:name]
]
)

Google pretty much answers this.
https://levelup.gitconnected.com/rails-nested-forms-in-three-steps-5580f0ad0e
https://guides.rubyonrails.org/form_helpers.html
You are looking for fields_for

Related

Can I have two different input fields and one column in the database?

I want to have two input fields, but only one column in the database.
The first input is stored data in numbers and the other one is stored data in numbers divided by 24.
You can put data only in one field.
Is there any possible way to do this?
UPD:
Migration:
def change
add_column :employees, :hourly_payment , :integer
end
View:
employees/_form.html.erb
<%= simple_form_for #employee do |form| %>
<%= form.input :name %>
<%= form.input :hourly_payment %>
<%= form.input :monthly_payment %>
<%= form.button :submit, class: "btn btn-success" %>
<% end %>
Your model and database tables are the internals of your application and are not actually tied to the view by anything except how easy ActiveRecord makes it to use convention over configuration to link the two*.
In Rails style MVC the controller is responsible for passing user input to the model. Usually you would just do this with simple mass assignment:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
# ...
end
private
def user_params
params.require(:user)
.permit(:email, :salary)
end
end
This is basically just passing a whitelisted hash of parameters straight to the model as is and it all gets passed to the setters that ActiveRecord magically created for you by reading the database schema.
But there is nothing stopping you from assigning attributes manually:
class UsersController < ApplicationController
def create
#user = User.new(user_params) do |user|
user.salary = calculated_salary
end
# ...
end
private
def user_params
params.require(:user)
.permit(:email)
end
def calculated_salary
if params[:user][:hourly_payment].present?
params[:user][:hourly_payment]
elsif params[:user][:monthly_payment].present?
params[:user][:monthly_payment].to_i / 168
else
0 # sorry no cookies for you
end
end
end
Or monkeying with the parameters object:
def user_params
params.require(:user)
.permit(:email)
.merge(salary: calculated_salary)
end
It is after all just a hash on steroids. The only thing that Rails will prevent you from is passing a parameters object that has not been whitelisted.
There is no stone tablet for what you can do in a controller. The only thing to bear in mind is that controllers are notoriously hard to test and fat controllers are a recipe for disaster.
If you're doing anything more complicated there are better solutions such as form objects, decorators or service objects.
You'll need to create a view for that. Here is an example of migration:
def up
sql = %(CREATE OR REPLACE VIEW my_view_models AS
SELECT m.*,m.int_field*12 as my_new_value from my_models m
)
self.connection.execute(sql)
end
def down
self.connection.execute('DROP VIEW IF EXISTS my_view_models')
end
Then, you can access your value with method my_new_value on your model. You'll need to change the name of the default table matching your model.
class MyModel
self.table_name = 'my_view_models'
end
And access it via
MyModel.first.my_new_value

How to access nested objects in strong params

Hello I'm quite new to rails API. I'm having trouble on how can I access the :guest object from the params. What I want to to do is to create a new record in Booking and Guest. Thank you.
Booking Controller
attr_accessor :guest
def create
booking = Booking.new(reservation_params)
booking.guest.build({booking_id: booking.id, first_name: ?, last_name: ?})
end
def reservation_params
params.require(:booking).permit(:start_date, :end_date :guest => [:first_name, :last_name])
end
POST
{
"start_date": "2021-03-12",
"end_date": "2021-03-16",
"guest": {
"first_name": "John",
"last_name": "Doe",
}
}
1. You're assigning a local variable - not an instance variable.
attr_accessor :guest
def create
booking = Booking.new(reservation_params)
end
Here you might assume that since you declared a setter with attr_accessor that this would set the instance variable #booking so that you can access it from the view? Wrong. When performing assignment you need to explicitly set the recipient unless you want to assign a local variable.
attr_accessor :guest
def create
self.booking = Booking.new(reservation_params)
end
But you could actually just write #booking = Booking.new(reservation_params) since that setter is not actually doing anything of note.
2. Models don't have an id until they are saved.
This line:
booking.guest.build({booking_id: booking.id, first_name: ?, last_name: ?})
Is actually equivilent to:
booking.guest.build(booking_id: nil, first_name: ?, last_name: ?)
One big point of assocations is that the ORM takes care of linking the records for you. Let it do its job. If you're ever assigning an id manually in Rails you're most likely doing it wrong.
3. You're not saving anything to the DB
.new (build is just an alias for new) just instanciates an new model instance. You need to actually save the object for it to have any effect beyond the current request.
How do I fix it?
If you want to use that parameter structure it can be done with a bit of slicing and dicing:
def create
#booking = Booking.new(reservation_params.except(:guest)) do |b|
b.guest.new(reservation_params[:guest])
end
if #booking.save
redirect_to #booking
else
render :new
end
end
The reason you use except(:guest) to remove the guest param is that the setter defined by the assocation expects an instance of guest and not a hash so it will blow up otherwise
Nested attributes
accepts_nested_attributes is the Rails way of passing attibutes through another model. It expects the parameter to be named guest_attributes not guest.
class Booking < ApplicationRecord
belongs_to :guest
accepts_nested_attributes_for :guest
end
If you really need to use the existing params structure you can just alter the parameters in your whitelisting method:
class BookingsController < ApplicationController
# ...
def create
#booking = Booking.new(reservation_params)
if #booking.save
redirect_to #booking
else
render :new
end
end
private
def reservation_params
params.require(:booking)
.permit(:start_date, :end_date, guest: [:first_name, :last_name])
.tap do |p|
# replaces the key :guests with :guest_attributes
p.merge!(guest_attributes: p.delete(:guest))
end
end
end
You're trying to create two associated models as once, you can use:
accepts_nested_attributes_for.
In the booking model add:
# models/booking.rb
accepts_nested_attributes_for :guest
And then in the controller:
def reservation_params
params.require(:boking).permit(:start_date,...,
guest_attributes: [:first_name, :last_name])
end
And then you can just create the booking including the guest like so:
booking = Booking.new(reservation_params)
Find more info here:
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Save external Tweets in database in Rails

I am new to rails developement and to the MVC architecture. I have a little application where I can add Videos' URLs from Dailymotion or Youtube and get the tweets related to that URL using the twitter gem in Ruby on Rails.
Now i'm able to store the tweets like this : (This is the video controller)
def show
#video = Video.find(params[:id])
# Creating a URL variable
url = #video.url
# Search tweets for the given video/url
#search = get_client.search("#{#video.url} -rt")
# Save tweets in database
#search.collect do |t|
tweet = Tweet.create do |u|
u.from_user = t.user.screen_name.to_s
u.from_user_id_str = t.id.to_s
u.profile_image_url = t.user.profile_image_url.to_s
u.text = t.text.to_s
u.twitter_created_at = t.created_at.to_s
end
end
I'm not sure if this is the right way to do it (doing it in the controller ?), and what I want to do now is to specify that those tweets that have just been stored belong to the current video. Also I would like to have some sort of validation that makes the controller look in the database before doing this to only save the new tweets. Can someone help me with that ?
My models :
class Video < ActiveRecord::Base
attr_accessible :url
has_many :tweets
end
class Tweet < ActiveRecord::Base
belongs_to :video
end
My routes.rb
resources :videos do
resources :tweets
end
This is an example of a "fat controller", an antipattern in any MVC architecture (here's a good read on the topic).
Have you considered introducing a few new objects to encapsulate this behavior? For example, I might do something like this:
# app/models/twitter_search.rb
class TwitterSearch
def initialize(url)
#url = url
end
def results
get_client.search("#{#url} -rt")
end
end
# app/models/twitter_persistence.rb
class TwitterPersistence
def self.persist(results)
results.map do |result|
self.new(result).persist
end
end
def initialize(result)
#result = result
end
def persist
Tweet.find_or_create_by(remote_id: id) do |tweet|
tweet.from_user = screen_name
tweet.from_user_id_str = from_user_id
tweet.profile_image_url = profile_image_url
tweet.text = text
tweet.twitter_created_at = created_at
end
end
private
attr_reader :result
delegate :screen_name, :profile_image_url, to: :user
delegate :id, :user, :from_user_id, :text, :created_at, to: :result
end
Notice the use of find_or_create_by ... Twitter results should have a unique identifier that you can use to guarantee that you don't create duplicates. This means you'll need a remote_id or something on your tweets table, and of course I just guessed at the attribute name (id) that the service you're using will return.
Then, in your controller:
# app/controllers/videos_controller.rb
class VideosController < ApplicationController
def show
#tweets = TwitterPersistence.persist(search.results)
end
private
def search
#search ||= TwitterSearch.new(video.url)
end
def video
#video ||= Video.find(params[:id])
end
end
Also note that I've removed calls to to_s ... ActiveRecord should automatically convert attributes to the correct types before saving them to the database.
Hope this helps!

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: Method declared in Application controller, but undefined according to browser

... used in orderscontroller#new to create an array, which is used in a select method of a form field in views, which is supposed to add a new order
Hullo there,
So my error is this
Routing Error
undefined local variable or method `array_of_payment_types' for #
Now, I've got this select field for a form to submit a new order, but my browser won't load the page with the form:
<div class="field">
<%= f.label :pay_type %><br />
<%= f.select :payment_type_id, Order::PAYMENTS_TYPES,
:prompt => 'Select a payment method' %>
</div>
This is using an array that I am trying to create twice:
class Order < ActiveRecord::Base
...
belongs_to :payment_type
**PAYMENT_TYPES = array_of_payment_types**
validates :name, :address, :email
validates :pay_type, :inclusion => { :in => PAYMENT_TYPES }
...
**def array_of_payment_types
PaymentType.pluck(:pay_type_name)
end**
end
and here:
class OrdersController < ApplicationController
...
def new
#cart = current_cart
if #cart.line_items.empty?
redirect_to store_url, :notice => "Your cart is empty"
return
end
**#PAYMENT_TYPES = array_of_payment_types**
#hide_checkout_button = true
#order = Order.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #order }
end
end
...
end
while the method to create the array, which "plucks" the entries from the :pay_type_name column Payment_Type table, is declared both in order.rb and the ApplicationController, here:
class ApplicationController < ActionController::Base
protect_from_forgery
private
...
def array_of_payment_types
PaymentType.pluck(:pay_type_name)
end
end
Now I am trying to use the same process as other pieces of my application, just replicating stuff I have already done. For example, in OrdersController#new I've got
#cart = current_cart
current_cart is a method declared in the ApplicationsController and it works. So why doesn't array_of_payment_types also work?
Thanks for your help in advance :-)
Further information...
What I am trying to do with this is to create a new order, using a form, and one of the fields in the form enters a "pay_type" (or payment type in English). I want to present the user with options which is a list "plucked" from the entries in the PaymentType table, column :pay_type_name (I may be repeating myself, but no harm). But the new Order is created after the Order#new action, which is where I have created the array. Where/how should I create the array?
def array_of_payment_types in your Order class defines an instance method and you are trying to use it as a class method.
I'd just define array_of_payment_types as a class method instead and call Order.array_of_payment_types instead of the constant ARRAY_OF_PAYMENT_TYPES in your view.
You can always cache it in the class method, there's no need to use a constant for this.
def self.array_of_payment_types
#array_of_payment_types ||= PaymentType.pluck(:pay_type_name)
end
But, consider leaving the responsibility for the payment type array in the PaymentType class. The Order class shouldn't be the point of contact to retrieve data which is clearly under the control of another class.

Resources