Instance variables in ActiveSupport::Concern - ruby-on-rails

I have read that modules (in this case ActiveSupport::Concern) in ruby are shared amongst all instances of the class initialised. So if that were true it would mean any instance variables would be shared among all of the instances in memory.
module SetPassword
extend ActiveSupport::Concern
attr_reader :password
def password=(new_password)
#password = new_password
self.crypted_password = password_digest(new_password, salt)
end
end
class User
include SetPassword
end
u = User.new; u.password = 'abcdef'
u2 = User.new; u2.password = '123456'
Is the code above safe to use? Or would the second user override the first user?

Module#include is under the hood calling Module#append_features. That means, nothing is shared and this might be easily checked (ActiveSupport::Concern has nothing to do with the code you are to check.)
module SetPassword
attr_reader :password
def password=(new_password)
#password = new_password
puts <<~EOS
Setting password to: #{#password} (id: #{#password.__id__}).
Object: #{self.class.name} (#{self.__id__}).
Method: #{__callee__} on #{self.method(__callee__).__id__}.
EOS
end
end
class User
include SetPassword
end
u1 = User.new; u1.password = 'abcdef'
u2 = User.new; u2.password = '123456'
The code above demonstrates that everything (the password itself, the instance variable and even the method on it’s own) is different:
▶ u1 = User.new; u1.password = 'abcdef'
#⇒ Setting password to: abcdef (id: 46968741459360).
# Object: User (46968741459420).
# Method: password= on 46968741459040.
▶ u2 = User.new; u2.password = '123456'
#⇒ Setting password to: 123456 (id: 46968734231740).
# Object: User (46968734232020).
# Method: password= on 46968734230640.

Related

Customize reset_password_token for devise in Rails

what I want to do : setup an invite process, when a user invite a new user it creates a user (email and password) and send a welcome email to this new user with a link to reset his password.
My User model
def set_initial_password_reset!
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
save(validate: false)
#token_reset = raw
end
My InviteController contain
#user_invitee = User.new(email: invite_params[:email]) do |u|
u.password = SecureRandom.hex
# raise
end
#user_invitee.skip_confirmation!
#user_invitee.save
#user_invitee.set_initial_password_reset!
create_invite
if #invite.save!
InviteMailer.new_user_invite(#invite, edit_password_path(#resource = User.find_by(id:
#invite.recipient), reset_password_token: #token_reset)).deliver
redirect_to trip_trip_form(#trip)
when I "raise" into the User model in set_initial_password_reset! to analyze #token_reset, I have got a value, but in the InviteController that value is nil and I don't understand how to grab this value?
I have try other method that I saw on stackoverflow to implement that process like :
User model
def set_initial_password_reset!
self.reset_password_token = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_sent_at = Time.now.utc
save(validate: false)
end
and in InviteController
InviteMailer.new_user_invite(#invite, edit_password_path(#resource = User.find_by(id: #invite.recipient), reset_password_token: #resource.reset_password_token)).deliver
but the token generated was invalid. I should have a token like this : http://localhost:3000/users/password/edit?reset_password_token=i8t77fcdsj3PYRymVdEK
but I get a much longer token.
for info my mailer controller
class InviteMailer < ApplicationMailer
def new_user_invite(invite, edit_password_path)
#invite = invite # Instance variable => available in view
#new_user_registration_url = edit_password_path
mail(to: #invite.email, subject: 'Welcome to Travlr!')
#trip = Trip.find_by(id: #invite.trip_id)
#sender = User.find_by(id: #invite.sender)
end
Thanks for your help!
so the problem with set_initial_password_reset! is the you don't get the raw token value that will be later used to identify the resource in the password reset process.
look at the implementation of reset password token method from devise (below)
def set_reset_password_token
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
save(validate: false)
raw
end
it saves the encrypted generated token to the user object and returns the raw value.
This raw value then should be included in the mail/link that is going to be used be the recipient (the user) while confirming that indeed it's he/she who can reset the password.
so if we revisit the code samples you've posted the controller would look somewhat like below:
in model: (basically same as the devise method. the only difference is that devise method is private afaik)
def set_initial_password_reset!
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
save(validate: false)
raw
end
in controller:
#user_invitee = User.new(email: invite_params[:email]) do |u|
u.password = SecureRandom.hex
# raise
end
#user_invitee.skip_confirmation!
#user_invitee.save
token = #user_invitee.set_initial_password_reset!
create_invite
InviteMailer.new_user_invite(#invite, edit_password_path(User.find_by(id: #invite.recipient), reset_password_token: token)).deliver

Stack level too deep on user.save

I want to assign a confirmation code to my users while creating one. And I also titleize some columns before saving-updating them. So my user.rb looks like this (it may be a bit messy):
// user.rb
*** some code ***
before_save { titleize_column(:name)
titleize_column(:surname)
capitalize_column(:complaints)
capitalize_column(:education)
capitalize_column(:job)
capitalize_column(:complaintsdetails)
capitalize_column(:prediagnosis)
capitalize_column(:existingdiagnosis)
capitalize_column(:knownilnessesother)
capitalize_column(:usedmedicine)
capitalize_column(:operation)
capitalize_column(:trauma)
capitalize_column(:allergy)
capitalize_column(:otherhabits)
capitalize_column(:motherother)
capitalize_column(:fatherother)
capitalize_column(:siblingsother)
}
before_save :generate_confirmation_code
protected
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
update_attribute :confirmation_code, SecureRandom.urlsafe_base64(20)
update_attribute :confirmed, false
else
update_attribute :confirmed, true
end
end
end
protected
def capitalize_column(attr)
unless self[attr].nil?
self[attr] = Unicode::capitalize self[attr]
end
end
protected
def titleize_column(attr)
unless self[attr].nil?
words = self[attr].split
words.each_with_index do |v,i|
words[i] = Unicode::capitalize v
end
self[attr] = words.join(" ")
end
end
I'm using separate methods for titleizing and capitalizing columns because they may be nil when first creating a user, so I'm checking if it is null or not in those methods. This structure works fine on a normal signup with strong parameters. However, if I try to use twitter signup with the method below, it gives me the error 'stack level too deep' and I can see that it calls the generate_confirmation_code 123 times from the application trace and then these happens:
app/models/user.rb:83:in each'
app/models/user.rb:83:ineach_with_index'
app/models/user.rb:83:in titleize_column'
app/models/user.rb:20:inblock in '
app/models/user.rb:64:in generate_confirmation_code' (x123 times)
app/models/user.rb:101:infrom_omniauth'
app/controllers/socials_controller.rb:4:in `create'
// method for signing up/logging in a user from twitter
class << self
def from_omniauth(auth_hash)
if exists?(uid: auth_hash['uid'])
user = find_by(uid: auth_hash['uid'])
else
user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'], type: 'Patient')
user.password_digest = User.digest('111111')
user.name = auth_hash['info']['name']
user.location = get_social_location_for user.provider, auth_hash['info']['location']
user.avatar = auth_hash['info']['image']
user.url = get_social_url_for user.provider, auth_hash['info']['urls']
user.save! // THIS IS THE LINE 101!
conversation = Conversation.create()
user.conversation = conversation
admin = Admin.first
admin.conversations << conversation
user.progress = Progress.create(active_state:1)
end
user
end
I think I'm messing up by using before_save not properly, but do not know how to do it right. What am I doing wrong here?
update_attribute also fires the save callbacks, thereby looping the before_save infinitely, thus producing stack level too deep.
You can just simply assign values in a before_save callback methods, because they will simply be saved afterwards anyway. See the following:
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end
You are calling update_attribute inside before_save callback method, instead you can just assign values to attributes. The method signature generate_confirmation_code should be like below -
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end

Rails 4 create associations in class method

This works perfectly fine:
User.first.social_profiles.create!
On the other hand, this creates the social_profile but does not create the association between the two:
class SocialProfile < ActiveRecord::Base
def self.create_google( auth_info )
# if where(provider: auth_info["provider"], uid: auth_info["uid"]).empty?
create! do |google|
google.provider = auth_info["provider"]
google.uid = auth_info["uid"]
google.image_url = auth_info["info"]["image"]
google.email = auth_info["info"]["email"]
google.access_key = auth_info["credentials"]["token"]
google.refresh_token = auth_info["credentials"]["refresh_token"]
google.expires_at = Time.at(auth_info["credentials"]["expires_at"])
google.expires = auth_info["credentials"]["expires"]
end
# else
# where(provider: auth_info[:provider], uid: auth_info[:uid]).first
# end
end
end
Console:
2.1.2 :102 > User.first.social_profiles.create_google( ...the auth hash ...)
What's the problem here? How can I fix it?
This does work though
p = User.first.social_profiles.create_google(...the auth hash ...)
User.first.social_profiles << p
The User.first instance does not get carried into the SocialProfile.create_google method, and therefore the create! method wouldn't have the user instance available.
You can assign it yourself by passing it in:
class SocialProfile < ActiveRecord::Base
def self.create_google( user, auth_info )
create! do |google|
google.user_id = user.id,
...
end
end
end
And calling it with
SocialProfile.create_google( User.first, auth_info )
Alternatively, consider having the create_google_profile method in User, so that you can
class User < ActiveRecord::Base
def create_google_profile( auth_info )
self.social_profiles.create(
provider: auth_info["provider"],
...
)
end
end
and calling it with
User.first.create_google_profile( auth_info )

How to test a specific line in a rails model using rspec

I have a model with an initializer in it, which basically creates a user from a user hash.
After it gets the user information, it checks whether the "privileges" key in the hash is an array. If it's not, it turns it into an array.
Now the obvious way of doing this would be crafting an entire user_hash so that it would skip those "create user" lines and then check if it turns the input into an array if necessary. However, I was wondering if there is a more DRY way of doing this?
Here is the user model I'm talking about:
def initialize(opts={})
#first_name = opts[:user_hash][:first]
#last_name = opts[:user_hash][:last]
#user_name = opts[:user_hash][:user_name]
#email = opts[:user_hash][:email]
#user_id = opts[:user_hash][:id]
#privileges = {}
if opts[:privs].present?
if !opts[:privs].kind_of?(Array)
opts[:privs] = [opts[:privs]]
end
end
end
You can pass a double which returns the needed value when the proper key is requested, and itself (or something else) otherwise:
it 'turns privs into an array' do
opts = double(:opts)
allow(opts)to receive(:[]).and_return(opts)
allow(opts)to receive(:[]).with(:privs).and_return('not array')
expect(MyClass.new(opts).privileges).to eq(['not array'])
end
Btw, your code could be simplified using the splat operator:
privs = [*opts[:privs]]
sample behavior:
privs = nil
[*privs]
# => []
privs = ['my', 'array']
[*privs]
# => ["my", "array"]
privs = 'my array'
[*privs]
# => ["my array"]
You can even use the idempotent Kernel#Array
def initialize(opts = {})
#first_name = opts[:user_hash][:first]
#last_name = opts[:user_hash][:last]
#user_name = opts[:user_hash][:user_name]
#email = opts[:user_hash][:email]
#user_id = opts[:user_hash][:id]
#privileges = {}
Array(opts[:privs])
end
I hope that helps
Rather than testing the implementation (value is turned into an array), I would test the desired behavior (takes single privilege or multiple privileges):
describe User do
describe '#initialize' do
it "takes single privilege" do
user = User.new(user_hash: {}, privs: 'foo')
expect(user.privileges).to eq(['foo'])
end
it "takes multiple privileges" do
user = User.new(user_hash: {}, privs: ['foo', 'bar'])
expect(user.privileges).to eq(['foo', 'bar'])
end
end
end

inject method to retrieve data from hash

I'm having trouble getting the method below in my user model to handle a hash ('auth') I'm getting from LinkedIn for user signin:
def self.deep_get auth, *fields
auth.inject(auth) { |acc, e| acc[e] if acc }
end
I call the 'deep_get' method later in my user model as I create a user using omniauth/linkedin gem. However, it's returning nil values for the provider/uid/headline/email user fields that I know are not nil.
I included first_name and last_name fields as an example because this approach is working (not returning nil values), but (as I realize) bad style/exception handling. Any ideas as to why my deep_get inject method isn't working to retrieve the data in the hash as I'd like it to?
def self.create_from_omniauth(auth)
create! do |user|
# i'd like to retrieve user information from linkedin per the following with my inject method, but i am getting nil values when i should be getting data.
# :provider and :uid are on the same branch level of data. first_name,last_name,email,etc. are on a branch just below called 'info'
user.provider = deep_get(auth, :provider)
user.uid = deep_get(auth, :uid)
user.headline = deep_get(auth, :info, :headline)
user.email = deep_get(auth, :info, :email)
# the below is working but i know pokemon exception handling is not good style.
begin
user.first_name = auth["info"]["first_name"]
rescue
end
begin
user.last_name = auth["info"]["last_name"]
rescue
end
try this
def deep_find(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=deep_find(a.last,key) }
r
end
end
or try this
class Hash
def deep_fetch(key, default = nil)
default = yield if block_given?
(deep_find(key) or default) or nil
end
def deep_find(key)
if key?(key)
self[key]
else
self.values.inject(nil) do |memo, v|
memo = v.deep_find(key) if v.respond_to?(:deep_find)
memo unless memo.nil?
end
end
end
end

Resources