rails autosave nested model selected with where/find_by - ruby-on-rails

i have a special case for which i need to know the best practice.
Given a simple has_many association:
class Authentication < ActiveRecord::Base
belongs_to :user
#provider can be :password, :facebook_oauth etc
#code is the encrypted password on provider == :password
end
class User < ActiveRecord::Base
has_many :authentications
#this works
def encrypted_password=(pw)
set = false
self.authentications.each do |auth|
if auth.provider.to_sym == :password
set = true
auth.code = pw
end
end
self.authentications.build(provider: :password, code: pw) unless set
pw
end
#this only when no password-auth exist yet
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).code = pw
end
end
and then
user = User.last
user.password="abcdefg"
user.save
While the first solution works, it loads and iterates over ALL associated Authentication objects. It was a workaround but this is a no-go.
The second solution does not work when it loads an existing Password-Authentication object. The User object does not know about the change on the Authentication object loaded with the find_or_initialize_by method. The change won't be saved...
Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?

It seems saving associating object returned with find back to parent object is impossible as of now. Refer to this issue https://github.com/rails/rails/issues/17466.
I had the same issue, and my workaround was, even though this is not you nor I wanted, to use save in the method yourself and make all the saves inside the transaction.
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).update_attribute(code, pw)
end

Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?
If your question only consists of needing to know how to save an associated class, you can add this to your class definition:
class User < ActiveRecord::Base
has_many :authentications, autosave: true
end
The Authentication object is already referenced back to the User object via the user_id column that should be on Authentication by way of the belongs_to method. This autosave: true will save the associated object Authentication when the parent object (User) is saved.

Related

Rails update associated model's object

My Opportunity model looks like this:
class Opportunity < ApplicationRecord
# opportunity belongs to a user
belongs_to :user
def self.create_opportunity(params)
# Fetch opportunity params and user params from request parameters
opportunity_params = params[:opportunity_params]
user_params = params[:user_params]
opportunity = Opportunity.find_or_initialize_by(id: opportunity_params[:id])
opportunity.assign_attributes(opportunity_params)
opportunity.user = User.find_or_initialize_by(email: user_params[:email])
opportunity.user.assign_attributes(user_params)
opportunity.save
end
user.rb model
class User < ApplicationRecord
# validate user email
validates :email, presence: true, email: true
enum gender: { male:1, female:2 }
end
We create a new user if a user with the email provided does not exists. This works well when a new user is created, but doesn't work when there already is a user. The update for user model doesn't work.
For update on user to work, I need to specifically call opportunity.user.save
Any idea how to make this work without explicitly calling save on user model?
With this line you can create a new user if it does not exist and update its data.
opportunity.user.find_or_create_by(email: user_params[:email]).update(user_params)
This method will return true/false if the user has been updated or not. If you want the user created itself use this:
opportunity.user.find_or_create_by(email: user_params[:email]).tap{ |user| user.update(user_params) }
edit:
THIS HAS TO WORK:
opportunity.user = User.find_or_create_by(email: user_params[:email]).tap{ |user| user.update(user_params) }

Referencing Associated Object Returns Nil

I have two models.
class User < ActiveRecord::Base
has_one :message
end
class Message < ActiveRecord::Base
belongs_to :user
end
If I have a created user with an associated Message and I delete that message and create a new one like, user.message returns nil. For example.
user = User.create
message = Message.create(user_id: user.id)
Message.where(user_id: user.id).destroy_all
Message.create(user_id: user.id)
# Now if I call this below, it always returns nil
user.message
Why does this occur? Shouldn't Rails 3 pick up on that change? How do I fix this?
Just load the object again before doing user.message like, user.reload.
reload - Reloads the record from the database.

Grab certain variable from model to model relation in Rails

I'm trying to grab a variable in my user model that's in relation to authorization model. The following works in Rails console but not sure how to translate that into the user.rb model since it keeps giving me error that it has to be a symbol or string. Doing to_s isn't correct outcome.
class User < ActiveRecord::Base
has_many :authorizations
def social
user = User.current
token = user.authorizations.pluck(:token).to_s
end
end
my authorizations model also looks like this;
class Authorization < ActiveRecord::Base
belongs_to :user
end
I've tried doing
class User < ActiveRecord::Base
def social
user = User.current
token = user.authorizations.select(:token).to_s
end
end
Doing this below in console works but not when I'm doing user.authorizations
auth = Authorization.where(:user_id => '54').token
but everything comes incorrect because it either has brackets or \ slashes within it.
If it's returning the user.authorizations then do a
user.authorizations.collect(&:token).first.to_s
pluck is on the db level. It will only query the particular field.
You are close with:
token = user.authorizations.pluck(:token).to_s
To align with Like #AnkitG's answer it would need to be
token = user.authorizations.pluck(:token).first.to_s
If you are going to consistently have a single authorization you may want to change the relationship from
has_many :authorizations
to
has_one :authorization
This way you can:
user.authorization.token

Validate Associated Object Presence Before Create

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 %>

attr_accessor - Ruby - where it saves the values

I have this model:
class User < ActiveRecord::Base
attr_accessible :subscription_process
def self.prepare_user
user = User.new
user.subscription_process = true
user.save
end
end
Inn the email that is send to the user - I use devise I have subscription_process that is equal to true. I want to know if subscription_process is saved somewhere?
Don't confuse attr_accessor and attr_accessible - those are two completely different things.
As for the question, the value is stored in the database.
user.subscription_process = true
user.save # here, it gets saved.
When you say obj.save then the it would be inserted in database and the values would be hold in that object. In your case when you save it, it will insert in Users table in database and the values are available in user object with id.
To understand attr_accessible and attr_accessor please go through this link:
Difference between attr_accessor and attr_accessible
Hope this helps !!!

Resources