Rails update associated model's object - ruby-on-rails

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) }

Related

Modify the User's DB via code in users_controller.rb

I have two users, let's call them UserA and UserB. UserA sends friend request to UserB. This results in creation of a new attribute called as 'Friendtokens' in the UserB's database. The database used is MongoDB.
Friendtokens: {"919839398393"=>{"atoken"=>"f704e803061e594150b09ad8acabfc6105ac85ab", "confirmed"=>true}}
Now, when UserA, decides to delete UserB, I need to modify Friendtokens of UserB's DB and delete the UserA's mobile no. from it.
Any guidance of how could I achieve this by writing a simple code in users_controller.rb would be highly appreciated.
You are providing little info. Here's a modelisation I can suggest :
class User
include Mongoid::Document
embeds_many :friend_tokens
field :phone_number
class FriendToken
include Mongoid::Document
embedded_in :user
belongs_to :friend, class_name: "User"
field :confirmed, type: Boolean
def phone_number
confirmed ? friend.phone_number : "Friend not confirmed, not showing user phone number !"
end
users_controller.rb
def create_friend_request
#user.friend_tokens << FriendToken.create(friend_request_params)
end
def confirm_friend_request
token = #userA.friend_tokens.select{|t| t.friend == #userB}.first
if token
token.confirmed = true
token.save
else
# Error message
end
end
def delete_friend
friend_for_A = #userA.friend_tokens.select{|t| t.friend == #userB}.first
# Check it has been found
if friend_for_A
# Delete friend token for A
friend_for_A.delete
# Switch to unconfirmed for user B (we can also assume the token exists)
friend_for_B = #userB.friend_tokens.select{|t| t.friend == #UserA}.first
friend_for_B.confirmed = false
friend_for_B.save
else
# Error messages
end
end

rails autosave nested model selected with where/find_by

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.

Keeping the same attribute in sync on two models in a Rails 2.x application?

I'm working in a large Rails 2.3 application and I have data on a model that would like to move to another model. I need to do this is phases as there are places in the Rails code base that are reading and writing this model data and outside applications reading the table data directly via SQL. I need to allow a period of time where the attribute is synchronized on both models and their associated tables before I drop one model and table altogether.
My models have a has_one and belongs_to relationship like this:
class User < ActiveRecord::Base
has_one :user_email, :inverse_of => :user
accepts_nested_attributes_for :user_email
validates_presence_of :email
def email=( value )
write_attribute(:email, value)
user_email.write_attribute(:email, value)
end
end
class UserEmail < ActiveRecord::Base
belongs_to :user, :inverse_of => :user_email
validates_presence_of :email
def email=( value )
write_attribute(:email, value)
user.write_attribute(:email, value)
end
end
I'd like to do away with UserEmail and its associated table altogether, but for a time I need to keep email up-to-date on both models so if it's set on one model, it's changed on the other. Overriding email= on each model is straightforward, but coming up with a commit strategy is where I'm hitting a wall.
I have places in the code base that are doing things like:
user.user_email.save!
and I'm hoping to find a way to continue to allow this kind of code for the time being.
I can't figure out a way to ensure that saving an instance of User ensures the corresponding UserEmail data is committed and saving an instance of UserEmail ensures the corresponding User instance data is also committed without creating an infinite save loop in the call backs.
This is the flow I would like to be able to support for the time being:
params = { user: { email: 'foo#bar.com', user_email: { email: 'foo#bar.com' } } }
user = User.create( params )
user.email = "moo#bar.com"
user.save
puts user.user_email # puts "moo#bar.com"
user.user_email.email = "foo#bar.com"
user.user_email.save
user.reload
puts user.email # puts "foo#bar.com"
Is there a way to achieve this sort of synchronization between the User and UserEmail models so they are kept in sync?
If it helps, I can probably do away with accepts_nested_attributes_for :user_email on User.
Using ActiveModel::Dirty
In User model
after_save :sync_email, :if => :email_changed?
def sync_email
user_email.update_column(:email, email) if user_email.email != email
end
In UserEmail model
after_save :sync_email, :if => :email_changed?
def sync_email
user.update_column(:email, email) if user.email != email
end
Let's assume, for sanity's sake, that the models are "User" and "Cart", and the shared field is "email". I would do this:
#in User
after_save :update_cart_email
def update_cart_email
if self.changes["email"]
cart = self.cart
if cart.email != self.email
cart.update_attributes(:email => self.email)
end
end
end
#in Cart
after_save :update_user_email
def update_user_email
if self.changes["email"]
user = self.user
if user.email != self.email
user.update_attributes(:email => user.email)
end
end
end
Because we check if the other model's email has already been set, it shouldn't get stuck in a loop.
This works if you drop accepts_nested_attributes_for :user_email -- otherwise you'll get a save loop that never ends.

Rails: passing params hash to model

I have a user-to-user messaging system. I'm trying to pass an array of user ids to a ConversationUser (join table) model which would then create multiple conversation_users from each individual user.id. The two fields in ConversationUser are conversation_id and user_id. I'm able to initialize a single conversation user because the new conversation_id is being passed along to the model, but for some reason, the hash of user ids is not getting to my model. I'm getting a Validation failed: User can't be blank
My conversation/new view for capturing the user_ids:
<%= check_box_tag "conversation_user[recipient][]", user.id %> <%= user.name %><br />
I know this is working because part of my params that I'm receiving back are:
"conversation_user"=>{"recipient"=>["9", "10"]}
The essentials of my Rails 4 controller & strong params:
class ConversationsController < ApplicationController
def new
#user = User.find(params[:user_id])
#conversation = #user.conversation_users.build
#conversation.build_conversation.messages.build
end
def create
#conv = Conversation.create!
#conversation = #conv.conversation_users.create!(conversation_user_params)
end
def conversation_user_params
params.require(:conversation_user).permit(recipient: [])
end
The essentials of my ConversationUser model:
class ConversationUser < ActiveRecord::Base
attr_accessor :recipient
before_create :acquire_conversation
validates :user_id, :conversation_id, presence: true
def acquire_conversation
unless recipient.blank?
recipient.each do |u|
ConversationUser.create(user_id: u, conversation: conversation)
end
end
end
end
I think the problem is somewhere in my controller's conversation_user_params. But it also might be in the model's before_create method. I've been trying to fix this problem for a day now, with lots of debugging with no success. If anyone can be of assistance, I thank you in advance.
The problem is in the model. before_create callback is called before creating a ConversationUser. Let's name this created ConversationUser as CURRENT. So, before creating the CURRENT ConversationUser you loop through recipient ids and create a ConversationUser for each of them. The ConversationUsers that you are creating here are not CURRENT ConversationUser. CURRENT ConversationUser is saved after the callback is executed (after you create other ConversationUsers). But in this case CURRENT ConversationUser doesn't know wich User it belongs to, because you pass user_id parameter to ConversationUsers that you create in before_create callback, but you do not pass it to CURRENT ConversationUser when it is created (when original create! method is executed).
To solve this problem you can override original create! method or not use it at all for creating ConversationUsers by recipient ids. Add a new method to your Conversation model (for example create_conversation_users):
Solution
In the controller:
def create
#conv = Conversation.create!
#conversation = #conv.create_conversation_users!(conversation_user_params[:recipient])
end
In the model:
class Conversation
def create_conversation_users!(recipient_ids)
return if recipient_ids.blank?
recipient_ids.each do |recipient_id|
conversation_users.create!(user_id: recipient_id, conversation: self)
end
end
end
You should also update ConversationUser model:
class ConversationUser < ActiveRecord::Base
validates :user_id, :conversation_id, presence: true
end
The error is in the ConversationUser. before_create callbacks are ran before a record is created in the database BUT after validations are ran. To solve your issue, there's a few things you can do. One of them was answered by Chumakoff. Here's another option you can use.
Remove all the code inside ConversationUser and change conversation_user_params to
def conversation_user_params
params[:conversation_user][recipient].map do |recipient|
{ user_id: recipient }
end
end
What happens is you're passing an array of { user_id: 1 } to create! which is the same as calling multiple create!({ user_id: 1 }).

How to get id values after saving an ActiveRecord model in rails

I have an ActiveRecord model (I'm on rails 2.3.8) called user and I'm saving it as
user = User.new
user.name = "user name"
user.save!
what I want to do is get the generated user id after creating the user. But currently it returns true/ false value upon saving
How can I get the saved user id?
Just call user.id after you've saved.
For people who looking for on create in model, below is the way:
class User < ActiveRecord::Base
...
after_create :do_something
def self.do_something
user_id = id #this will have last created id
# use this according to your needs
end
...
end
the way I did it was
class User < ActiveRecord::Base
...
id = nil
after_save {id = self.id}
...
end
and then the local variable id is available throughout the model

Resources