I am having problems display an attribute in a belongs_to relationship. Lease belongs_to Unit. Unit has a column "number" in the db. When I try to display this attribute on the lease show page, it gives me an error of undefined method `number' for nil:NilClass. I feel like I'm doing something stupid and over looking it.
lease.rb
belongs_to :unit
unit.rb
has_many :leases
leases_controller.rb
def show
#lease = Lease.find(params[:id])
end
views/leases/show.html.erb
<%= #lease.unit.number %>
If I change my show page code to #lease.unit_id it will show the id of the unit. Here is a pic of my data base showing the Unit_id.
Your model may have unit_id attribute, but how about units that does not have that record with id = 1 have You checked?
Quick (dirty) fix:
<%= #lease.unit.number unless #lease.unit.nil? %>
you have to validate the Unit ID's presence on Lease model
validates :unit, presence: true
so your Lease model wouldn't be saved to database unless you've set the unit_id to Lease instance
now you may delete your existing Unit records from database, and start again.
you may need to add more integrity of each data, please read this
update
ok how about if "DBAs deleting records under some circumstancies" ?
you can delegate to Unit#number to Lease instance
delegate :number, to: :unit, prefix: true
now you can access the lease.unit.number even when the unit_id is nil, you can access it using lease.unit_number
Related
I’m using Rails 5. I have this model
class ConfidentialMemo < ApplicationRecord
belongs_to :scenario
belongs_to :scenario_role
end
In my view, I have this drop down set up for selecting a field to be populated into the “scneairo_role” field …
<%= f.collection_select :scenario_role, #scenario.roles, :id, :name, include_blank: false %>
In the create method of my controller, I have this
#confidential_memo = ConfidentialMemo.new(confidential_memo_params)
…
private
def confidential_memo_params
params.require(:confidential_memo).permit(:description, :scenario_id, :scenario_role)
end
but I’m getting the error
ScenarioRole(#70207639353420) expected, got String(#70207645188180)
What does this error mean and more importantly, what do I need to adjust tis save my model successfully?
Change :scenario_role to :scenario_role_id in your collection_select method and in your controller params method. Your form is outputting and sending an id (in the form of a string), but if you just try to assign an id to #note.scenario_role, it'll fail with the same type of message.
I've been following the Getting Started rails tutorial and am now trying some custom functionality.
I have 2 models, Person and Hangout. A Person can have many Hangouts. When creating a Hangout, a Person has to be selected and associated with the new Hangout. I'm running into issues however when I call my create action. This fires before the validate_presence_of for person.
Am I going about this the wrong way? Seems like I shouldn't have to create a custom before_create validation to make sure that a Hangout was created with a Person.
#hangout_controller
def create
#person = Person.find(params[:hangout][:person_id])
#hangout = #person.hangouts.create(hangout_params)
#hangout.save
redirect_to hangouts_path(#hangout)
end
#hangout.rb
class Hangout < ActiveRecord::Base
belongs_to :person
validates_presence_of :person
end
#person.rb
class Person < ActiveRecord::Base
has_many :hangouts
validates :first_name, presence: true
validates :met_location, presence: true
validates :last_contacted, presence: true
def full_name
"#{first_name} #{last_name}"
end
end
Create action fires before the validate_presence_of for person
I think you are confused about rails MVC. Your form contains a url and when you submit your form your form params are send to your controller action according to the routes you have defined in routes.rb Your controller action, in this case create action, interacts with model this is very it checks for your validations and if all the validations are passed your object is saved in databse so even though in your app the control is first passed to your controller but your object is saved only once if all the validations are passed.
Now lets comeback to your code. There are couple of things you are doing wrong
a. You don't need to associate your person separately:
In your create action you have this line:
#person = Person.find(params[:hangout][:person_id])
You don't need to do this because your person_id is already coming from your form and it'll automatically associate your hangout with person.
b. You are calling create method instead of build:
When you call .association.create method it does two things for you it first initialize your object, in your case your hangout and if all the validations are passed it saves it. If all the validations are not passed it simply rollback your query.
If you'll use .association.build it'll only initialize your object with the params coming from your form
c. Validation errors won't show:
As explained above, since you are calling create method instead of build your validation error won't show up.
Fix
Your create method should look like this:
def create
#hangout = Hangout.new(hangout_params) # since your person_id is coming from form it'll automatically associate your new hangout with person
if #hangout.save
redirect_to hangouts_path(#hangout)
else
render "new" # this will show up validation errors in your form if your hangout is not saved in database
end
end
private
def hangout_params
params.require(:hangout).permit(:person_id, :other_attributes)
end
You are confused with the controller and model responsibilities.
Let me try to explain what I think is confusing you:
First try this in your rails console:
Hangout.create
It shouldn't let you because you are not passing a Person object to the create method. So, we confirm that the validation is working fine. That validation means that before creating a Hangout, make sure that there is a person attribute. All this is at the model level, nothing about controllers yet!
Let's go to the controllers part. When the create action of the controller 'is fired', that controller doesn't know what you are trying to do at all. It doesn't run any validations. It is just an action, that if you want, can call the Hangout model to create one of those.
I believe that when you say 'it fires' you are saying that the create action of the HangoutController is called first than the create method on the Hangout model. And that is completely fine. The validations run at the model level.
Nested Attributes
I think you'll be better using accepts_nested_attributes_for - we've achieved functionality you're seeking before by using validation on the nested model (although you'll be able to get away with using reject_if: :all_blank):
#app/models/person.rb
Class Person < ActiveRecord::Base
has_many :hangouts
accepts_nested_attributes_for :hangouts, reject_if: :all_blank
end
#app/models/hangout.rb
Class Hangout < ActiveRecord::Base
belongs_to :person
end
This will give you the ability to call the reject_if: :all_blank method -
Passing :all_blank instead of a Proc will create a proc that will
reject a record where all the attributes are blank excluding any value
for _destroy.
--
This means you'll be able to create the following:
#config/routes.rb
resources :people do
resources :hangouts # -> domain.com/people/:people_id/hangouts/new
end
#app/controllers/hangouts_controller.rb
Class HangoutsController < ApplicationController
def new
#person = Person.find params[:people_id]
#hangout = #person.hangouts.build
end
def create
#person = Person.find params[:people_id]
#person.update(hangout_attributes)
end
private
def hangout_attributes
params.require(:person).permit(hangouts_attributes: [:hangout, :attributes])
end
end
Although I've not tested the above, I believe this is the way you should handle it. This will basically save the Hangout associated object for a particular Person - allowing you to reject if the Hangout associated object is blank
The views would be as follows:
#app/views/hangouts/new.html.erb
<%= form_for [#person, #hangout] do |f| %>
<%= f.fields_for :hangouts do |h| %>
<%= h.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
I have two models house and booking.Everything is okey over booking_date validation. But when I try to update or create multi booking in the same request. Validation can't check the invalid booking in the same request params.
Let give an example assume that booking table is empty.
params = { :house => {
:title => 'joe', :booking_attributes => [
{ :start_date => '2012-01-01', :finish_date => '2012-01-30 },
{ :start_date => '2012-01-15', :finish_date => '2012-02-15 }
]
}}
Second booking also save but its start_date is between first booking interval. When I save them one by one validation works.
class House < ActiveRecord::Base
attr_accessible :title, :booking_attributes
has_many :booking
accepts_nested_attributes_for :booking, reject_if: :all_blank, allow_destroy: true
end
class Booking < ActiveRecord::Base
belongs_to :house
attr_accessible :start_date, :finish_date
validate :booking_date
def booking_date
# Validate start_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.start_date, self.start_date, self.house_id).exists?
errors.add(:start_date, 'There is an other booking for this interval')
end
# Validate finish_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.finish_date, self.finish_date, self.house_id).exists?
errors.add(:finish_date, 'There is an other booking for this interval')
end
end
end
I google nearly 2 hours and could not find anything. What is the best approach to solve this problem?
Some resources
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
http://railscasts.com/episodes/196-nested-model-form-part-1
This was only a quick 15-minutes research on my part, so I may be wrong, but I believe here's the root cause of your problem:
What accepts_nested_attributes_for does under the hood, it calls 'build' for new Booking objects (nothing is validated at this point, objects are created in memory, not stored to db) and registers validation and save hooks to be called when the parent object (House) is saved. So, in my understanding, all validations are first called for all created objects (by calling 'valid?' for each of them. Then, again if I get it right, they are saved using insert_record(record,false) which leads to save(:validate => false), so validations are not called for the 2nd time.
You can look at the sources inside these pages: http://apidock.com/rails/v3.2.8/ActiveRecord/AutosaveAssociation/save_collection_association,
http://apidock.com/rails/ActiveRecord/Associations/HasAndBelongsToManyAssociation/insert_record
You validations call Booking.where(...) to find the overlapping dates-ranges. At this point the newly created Booking objects are still only in memory, not saved to the db (remember, we are just calling valid? for each of them in the loop, saves will be done later). Thus Booking.where(...) which runs a query against a DB doesn't find them there and returns nothing. Thus they all pass valid? stage and then saved.
In a nutshell, the records created together in such a way will not be cross-validated against each other (only against the previously existing records in the database). Hence the problem you see.
Thus either save them one-by-one, or check for such date-overlapping cases among the simultaneously created Bookings yourself before saving.
I am running Ruby (1.9.3) on Rails (3.2.0) and having a problem with the validation of virtual attributes.
I have a Flight model that represents a flight which, among others, has an attributes that represents the airport of departure and arrival.
Because the select for selecting an airport is potentially huge, I opted to go for an autocomplete solution, which is working perfectly fine. I am using a before_validation callback to properly populate the actual ID of the airport:
before_validation do
self.departure_airport = Airport.find_by_long_name(departure_airport_name)
self.arrival_airport = Airport.find_by_long_name(arrival_airport_name)
end
The problem is, however, that when the user enters the name of an airport that does not exist in the database, the commit fails because the ID of either airport is nil. Great. What's not great, however, is that this validation failure is not reflected on the form because technically, it's the input for another field:
validates :departure_airport, :arrival_airport presence: true
attr_accessor :departure_airport_name, :arrival_airport_name
<%= f.input :departure_airport_name %>
<%= f.input :arrival_airport_name %>
Is this even the way to properly go about, transforming the name of the airport into an ID in the before_validation callback? And if so, how can I get the validation errors to show up on the virtual name attribute of the airport?
I think you are going the right way with the before_validation callback.
You can validate virtual attributes like every normal attribute. So all you need is just some validation in the model. Try this:
validates :departure_airport, presence: true
validates :arrival_airport, presence: true
this should add an error to the objects errors and the error should be displayed in your form...
Lets say there is an error in shirt. will the error appear also in person.errors ?
and if so, how can i reach it ? ( i don't want to use person.shirt.errors)
class Person < ActiveRecord::Base
has_one : shirt
has_many : pants
validates :name, :presence => true
validates_length_of :name, :minimum => 3
end
person = Person.new(:name => "JD")
person.shirt.create(:color=> "red")
person.pants.create(:type=> "jeans")
person.valid?
According to this post it seems errors on child entities will be copied to the parent during a save, see here -> Validations section although this might have been changed since
Validations simply work as you'd expect;
#valid? will also validate nested models,
#save(false) will save without validations, etc.
The only thing to note is that all error messages
from the nested models are copied to the parent errors
object for error_messages_for. This will probably change
in the future, as discussed on the ticket, but that's
outside of the scope of this patch.
It should load into "person.errors". You can reach it by calling
person.errors.at(:<replace_this_with_name_of_attribute>)
You can also call
person.errors.each { |attr, msg| puts "attr = '#{attr}', msg = '#{msg}'" }
to check for all the error attribute names and the corresponding error messages. Good luck!