create rails nested object in one swoop - ruby-on-rails

I need an api key to save a user, and I need a user_id to save an api_key... Can I do both at once?
user.api_key = ApkiKey.generate_token
user.save
user.api_key.user_id = user.id
user.api_key.save

If the api_key has belongs_to relationship with user then following will work
user.api_key = ApkiKey.generate_token
user.api_key.user_id = user.id
user.save
the user.save will also trigger the user.api_key.save

I ended up doing the following:
#api_key.rb
before_create :generate_access_token
def generate_access_token
begin
self.access_token = SecureRandom.hex
end while self.class.exists?(access_token: access_token)
end
#user.rb
before_create do |user|
user.api_key = ApiKey.create(user_id: user.id)
end
The problem was that I didn't think I could access user.id before I created the user, but apparently it works. Thanks for the heads up #Hardik

Related

create calls before_save multiple times

I have a before_save callback in my model which encrypts 2 fields before they're saved to the database.
class Account < ActiveRecord::Base
before_save :encrypt_credentials, if: "!username.blank? && !password.blank?"
def encrypt_credentials
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
The situation is very similar to Devise models run before_save multiple times?. When I call Model.create!(...) - which includes the 2 fields that need to be encrypted, the before_save gets called twice, ending up in the fields being encrypted twice.
Account.create!(
{
username: ENV['USERNAME'],
password: ENV['PASSWORD']
})
Why is before_save called multiple times? I don't like the solution of the post linked above and I don't want to do new/build followed by save.
It was user error :( After calling account = Account.create!, I had other code which called save! back on the model: account.foo = bar; account.save!. This obviously called befor_save again and re-encrypted my fields. I ended up with something like this:
class Account < ActiveRecord::Base
before_save :encrypt_username, if: :username_changed?
before_save :encrypt_password, if: :password_changed?
def encrypt_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
end
def encrypt_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
Option 1 (could be a mistake in usage of callbacks):
Short answer: use after_save instead of before_save
Long answer: How to organize complex callbacks in Rails?
When you use the:
account = Account.new
account.save
You are firing the before_save hook each time.
Option 2 (could be a bug):
Maybe you're actually touching the record several times.
For example in:
def create
#account = Customer.find(params[:customer_id]).accounts.create(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
You are in fact touching it with create and save. In which case I suggest:
def create
#account = Customer.find(params[:customer_id]).accounts.build(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
Build doesn't try to save the record so you shouldn't have any more problems. Hope this helps! Have a great day!

Access current_user in model (or some other workaround)

I've read several SO links on this topic. Even if you can hack it to get current_user in model, you shouldn't do it. So, what are my options in my case?
I'm using the devise_invitable gem, and one of the commands is User.invite!({:email => email}, current_user), which stores who the user is invited by (current_user). I'd like to have this information.
Currently, users are invited to join a private group, and this process is handled in my group.rb model:
# group.rb
def user_emails
end
def user_emails=(emails_string)
emails_string = emails_string.split(%r{,\s*})
emails_string.each do |email|
user = User.find_for_authentication(email: email)
if user
self.add user
GroupMailer.welcome_email(user)
else
User.invite!(email: email) # But I want this: User.invite!({:email => email}, current_user)
user = User.order('created_at ASC').last
self.add user
end
end
end
If relevant, it's just a text_area that receives these emails to process:
# groups/_form.html.erb
<%= f.text_area :user_emails, rows: 4, placeholder: 'Enter email addresses here, separated by comma', class: 'form-control' %>
Without having to re-arrange too much, how can I run User.invite!({:email => email}, current_user) in this process, so that this useful information (who is invited by whom) is stored in my database? Much thanks!
Update:
With #Mohamad's help below, I got it working.
# group.rb
def emails
end
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.group_invite user
else
add User.invite!({:email => email}, inviter)
end
end
end
# groups_controller.rb
def update
#group = Group.friendly.find(params[:id])
if #group.update_attributes(group_params)
emails = params[:group][:emails]
#group.invite_many(emails, current_user) # also put this in #create
redirect_to #group
else
flash[:error] = "Error saving group. Please try again."
render :edit
end
end
And then nothing in my User model because User.invite is defined already by devise_invitable and I didn't need to do anything else. This process is working great now!
There are some subtle issues with your code. There's a potential race condition on the else branch of your code where you try to add the last created user. I'm also unsure that you need a setter method here unless you are access emails from elsewhere in the instance of Group.
As suggested by others, pass the current user as an argument form the controller. I'm not sure how invite! is implemented, but assuming it returns a user, you can refactor your code considerably.
I would do somethng like this:
def invite_many(emails, inviter)
emails.split(%r{,\s*}).each do |email|
if user = User.find_for_authentication(email: email)
add user
GroupMailer.welcome_email user
else
add User.invite!(email, inviter)
end
end
end
# controller
#group.invite_many(emails, current_user)
# User.invite
def invite(email, inviter)
# create and return the user here, and what else is necessary
end
If you are calling user_emails() from the controller (and I'm guessing you are as that must be where you are receiving the form to pass in emails_string), you can pass in the current_user:
user_emails(emails_string, current_user)
and change user_emails to receive it:
def user_emails=(emails_string, current_user)
You can store the current_user with global scope ,like #current_user,which can be assigned in sessions controller,so in model you will just #current_user as the current user of the app.

rails: force a model to be not valid

I have a very specific situation where I want to force an instance of a model not valid.
Something like this:
user = User.new
user.valid? #true
user.make_not_valid!
user.valid? #false
Any way to achieve that?
Thanks!
You can do:
validate :forced_to_be_invalid
def make_not_valid!
#not_valid = true
end
private
def forced_to_be_invalid
errors.add(:base, 'has been forced to be invalid') if #not_valid
end
Another variant that I found useful for testing:
invalid_instance = MyModel.new
class << invalid_instance
validate{ errors.add_to_base 'invalid' }
end

Ruby on Rails no such column: authentication.provider: Omniauth

I was following this tutorial on Omniauth: http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast
I keep getting this error:
no such column: authentication.provider:
Now the main thing I want to know is why "provider" isn't being accepted. It exists in the class... the authentications database exists... so why is it saying it isn't there?
Here's my authentications controller:
class AuthenticationsController < InheritedResources::Base
def index
#authentications = current_user.authentications if current_user
end
def create
#user = User.where(authentication: auth).first_or_create(:provider => auth['provider'], :uid => auth['uid'])
self.current_user = #user
# auth = request.env["omniauth.auth"] current_user.authentications.create(:provider => auth['provider'], :uid => auth['uid'])
flash[:notice] = "Authentication successful."
redirect_to authentications_url
end
def auth
request.env['omniauth.auth']
end
def destroy
#authentication = current_user.authentications.find(params[:id])
#authentication.destroy
flash[:notice] = "Successfully destroyed authentication."
redirect_to authentications_url
end
end
I can assure you I have a model called authentication and that this model has a provider and uid field. I've also tried where(authentications: auth) and where(auth: auth)
each with no luck.
Any ideas would be appreciated.
UPDATE
authentication.rb (model)
class Authentication < ActiveRecord::Base
attr_accessible :create, :destroy, :index, :provider, :uid, :user_id
belongs_to :user
end
UPDATE 2
I'm basically attempting to adapt this tutorial to rails 3.2.
The original line from the tutorial is commented out above.
UPDATE 3
Here is the entire first line of error:
SQLite3::SQLException: no such column: authentication.provider: SELECT "users".* FROM "users" WHERE "authentication"."provider" = 'facebook' AND "authentication"."uid" = '2222222' AND "authentication"."info" = '--- !ruby/hash:OmniAuth::AuthHash::InfoHash
Hate to be a burden... but the clock's really ticking, my ass is on the line, and I'm about to go completely insane trying to figure this out. If you can tell me just why provider isn't being accepted I'm sure I can figure out the rest.
your create action has not sense
User.where(authentication: auth) converts to SELECT * FROM users WHERE authentication = a_hash
You shoul do something like
auth1 = Authentication.where(provider: auth['provider'], uid: auth['uid']).first
if !auth1.nil?
current_user = auth.user
else
user = User.new
user.authentications.build(provider: auth['provider'], uid: auth['uid'])
user.save!
current_user = user
end
Since you are just adding a record in the authentications table, I am unable to understand why you are reassigning this.current_user. Also is current_user a helper method or a member, if it's a member where is it declared?
Don't you just want to create an authentication for the current user as such?:
def create
current_user.authentications.first_or_create(:provider => auth['provider'], :uid => auth['uid'])
flash[:notice] = "Authentication successful."
redirect_to authentications_url
end
This finds the first authentication record by provider and uid, if not found then creates that authentication record.
Also by that error, I hope you have figured out the answer to this question:
Now the main thing I want to know is why "provider" isn't being
accepted. It exists in the class... the authentications database
exists... so why is it saying it isn't there?
It is because you are calling first_or_create() on User object, not Authentication.
I also faced this issue recently. At first I thought I had forgotten to add a provider column to users table, but that wasn't it.
This is how I eventually solved it:
def self.from_omniauth(auth)
where(provider: auth["provider"], uid: auth["uid"]).first_or_create do |user|
user.email = auth["info"]["email"]
user.password = Devise.friendly_token[0, 20]
user.logo = auth["info"]["image"]
# if you use confirmable, since facebook validates emails
# skip confirmation emails
user.skip_confirmation!
end
end
auth is a hash like the one below, so instead of auth.provider, I used auth["provider"] etc:
omniauth.auth: {"provider"=>"facebook", "uid"=>"11111111111111", "info"=>{"email"=>"some#email.com", "image"=>"http://graph.facebook.com/v2.6/11111111111111/picture"}, "credentials"=>{"token"=>"sometoken", "expires_at"=>1506680013, "expires"=>true}, "extra"=>{"raw_info"=>{"email"=>"some#email.com", "id"=>"11111111111111"}}}

Creating a user profile with Omniauth hash

I'm trying to pull out profile information from my user model. Currently I'm using Omniauth (twitter & facebook) to create users and just writing all the information to the user table... this works great, however I was told that proper Rails convention is to separate the user & profile.
After a couple of days of trial and error, I'm having trouble getting the has_one relationship working while creating a profile with the users info from the Omniauth hash.
I've tried to user a before_filter to automagically create a profile before the user is create, which properly creates the user_id foreign key in the profiles table, however I can't seem to get any of the omniauth hash data to write.
my sessions controller:
def create
auth = request.env["omniauth.auth"]
if user = User.find_by_provider_and_uid(auth["provider"], auth["uid"])
redirect_to root_url, :notice => 'Signed In'
else
user = User.create_with_omniauth(auth)
redirect_to profile_path(user), :notice => 'Please verify your profile'
end
in my user model:
before_create :create_profile
def self.create_profile(auth)
create! do |profile|
profile.user_id = session[:user_id]
profile.name = auth["info"]["name"]
profile.email = auth["info"]["email"]
profile.nickname = auth["info"]["nickname"]
profile.location = auth["info"]["location"]
profile.image = auth["info"]["image"]
end
end
...
The other option I tried was to write the profile data with the profile model... which works for the Omniauth hash data, but wont write the correct user_id (so no foreign key)
profile.rb
def self.create_profile_with_omniauth(auth)
create! do |profile|
profile.user_id = session[:user_id]
profile.name = auth["info"]["name"]
profile.email = auth["info"]["email"]
profile.nickname = auth["info"]["nickname"]
profile.location = auth["info"]["location"]
profile.image = auth["info"]["image"]
end
end

Resources