I'm on railcasts just practicing some rails and have come across something I'm trying to understand.
I didn't get what the "self" on the authenticate method was doing. So I deleted it and tested the login of my app to see if it would show an error and it did:
error:
**NoMethodError in SessionsController#create
undefined method `authenticate' for #<Class:0x00000102cb9000**>
I would really appreciate if someone could explain exactly what that "Self" is doing. I was trying to figure out exactly what was going on but can't get my head around it.
Method is defined in model and called in sessions_controller..
I've been continuously deleting my app and starting from scratch to get the hang of it and many things make sense to me each time i start again but I'm stuck at "self".
I'm just the type of person who likes to understand why something works.
controller:
def create
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
redirect_to root_path, :notice => "Logged In"
else
flash.now.alert = "Invalid credentials"
render "new"
end
end
model:
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
This is a basic ruby question. In this case, self is used to define a class method.
class MyClass
def instance_method
puts "instance method"
end
def self.class_method
puts "class method"
end
end
Which are used like this:
instance = MyClass.new
instance.instance_method
Or:
MyClass.class_method
Hope that clears things up a little bit. Also refer to: http://railstips.org/blog/archives/2009/05/11/class-and-instance-methods-in-ruby/
self defines a method of the class instead of the instance of the class. So with def self.authenticate you can do the following:
u = User.authenticate('email#domain.com','p#ss')
Instead of doing…
u = User.new
u.authenticate('email#domain.com','p#ss')
That way, you don't have to create an instance of user to authenticate one.
For the sake of completion and to thwart future headaches, I'd like to also point out that the two are equivalent:
class User
def self.authenticate
end
end
class User
def User.authenticate
end
end
Matter of preference.
class User
def self.xxx
end
end
is one way of defining class method while
class User
def xxx
end
end
will define an instance method.
If you remove the self. from the def, you will get a method not found error when you do
User.authenticate
because you are trying to call a method on a class rather than an instance of the class. To use an instance method, you need an instance of a class.
Related
I'm trying to fully understand a line of code in the action mailer that is shown in every documentation but not really explained.
def welcome_email(user)
#user = user #don't understand exactly which user this is
mail(to: #user.email, subject: 'Do you have any spam?')
end
I thought you had to define that variable like User.find(params[:id]) or User.first or something else that retrieves a specific user. What does plain 'user' mean in this context?
Thanks for your help with this beginner level question.
#user = user
This line is setting the value of #user instance variable to user which is being passed as a variable to the welcome_email method.
#user instance variable can be accessed in the views linked to this mailer.
Where-ever the welcome_email method is called, it is likely to have set the value of user using user = User.find(params[:id]) or user = User.first or something similar, and that user is passed in as a parameter with welcome_email(user) call.
Assuming the mailer is called Notifier, and the welcome email has to be sent when the user signs up, the following code is likely to be in the app/controllers/users_controller.rb file:
class UsersController < ApplicationController
...
def create
...
#user = ...
Notifier.welcome_email(#user).deliver_now
...
end
...
end
So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.
I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.
User logs in via FB
The first login creates a user node. If the user exist, it simply retrieves that user+node
After the user node is created, the koala gem is used to access the FB Graph API
Retrieves friendlist of each friend using the app.
Go through each friend and add a two way friendship relationship between the two users
As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?
SessionsController for reference
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
So in my user.rb I have something like this
has_many :both, :friendships
after_save :check_friends
def self.from_omniauth(auth)
#user = User.where(auth.slice(:provider, :uid)).first
unless #user
#user = User.new
# assign a bunch of attributes to #user
#user.save!
end
return #user
end
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
friendship.rb
from_class User
to_class User
type 'friendship'
def self.create_friendship(user,friend_node)
friendship = Friendship.create(from_node: user, to_node: friend_node)
end
I'm not sure if I'm on the right track with how to create a relationship node. As I just created #user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.
Right now it doesn't know that user and friend_user are nodes
If you see other bad code practice, please let me know!
In advance: Thanks for the help #subvertallchris. I'm sure you will be answering lots of my questions like this one.
This is a really great question! I think that you're on the right track but there are a few things you can change.
First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
You'll run into some problems otherwise.
You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.
As for handling your big problem, there's a lot you can do here.
EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
#user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
session[:user_id] = #user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
class User
include Neo4j::ActiveNode
# lots of other properties
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
def self.from_omniauth(auth)
User.where(auth.slice(:provider, :uid)).limit(1).first
end
def self.create_from_omniauth(auth)
user = User.new
# assign a bunch of attributes to user
if user.save!
user.check_friends
else
# raise an error -- your user was neither found nor created
end
user
end
# more stuff
end
That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.
But we're not done. Let's look at your original check_friends:
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.
def check_friends
facebook.get_connection("me", "friends").each do |friend|
friend_node = User.find_by(facebook_id: friend["id"])
Friendship.create_friendship(user,friend_node) unless friend_node.blank?
end
end
The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:
def self.create_friendship(user, friend_node)
Friendship.new(from_node: user, to_node: friend_node).save
end
create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.
At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.
I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.
# models/concerns/facebook.rb
module Facebook
extend ActiveSupport::Concern
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
end
# now back in User...
class User
include Neo4j::ActiveNode
include Facebook
# more code...
end
It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!
This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.
I implemented
gem 'devise_invitable'
for model User and I am facing an issue while inviting an existing user. The error says "USER IS ALREADY REGISTERED". I would like to add the same user in another User invited list. How can this be done?
For those looking for a different implementation of the same problem, you can add the new behavior to the InvitationsController protected method, invite_resource.
A more verbose explanation of the example below can be found on the DeviseInvitable wiki page, titled Invite a Resource (or User) that Has Already Signed Up without Invitation.
class Users::InvitationsController < Devise::InvitationsController
protected
# invite_resource is called when creating invitation
# should return an instance of resource class
# this is devise_invitable's implementation
# def invite_resource(&block)
# resource_class.invite!(invite_params, current_inviter, &block)
# end
def invite_resource(&block)
#user = User.find_by(email: invite_params[:email])
# #user is an instance or nil
if #user && #user.email != current_user.email
# invite! instance method returns a Mail::Message instance
#user.invite!(current_user)
# return the user instance to match expected return type
#user
else
# invite! class method returns invitable var, which is a User instance
resource_class.invite!(invite_params, current_inviter, &block)
end
end
end
To accomplish this, you'll need to create a new Invitations Controller that inherits from the original Devise::Invitations controller, but has modified logic in the create method.
The gem's README has a section on "Configuring Controllers" which describes this process. I also suggest having a look at the source code for the parent controller as it will help provide some context.
I did something similar to what you desire, and used the Rails built-in method of find_by_email. Here's some of the code I used...
def create
# new user
if User.find_by_email(invite_params[:email]).nil?
super
# existing user
else
#u = User.find_by_email!(invite_params[:email])
....more code that does what you want....
end
end
NOTE: Rails is smart and will use logic from the parent controller if no conflicting instructions are given the child controller you create. The point is that you don't need to re-write the whole controller. Ideally, you'll just make your modifications in the child controller then call super to revert back to the same method in the parent controller to finish the action.
I am following the example write in Chapter 14 "Logging In" of the book.
I have my view in "127.0.0.1:3000/login" working well, but if i insert my user and password it returns this error:
NoMethodError in SessionsController#create
undefined method `authenticate' for #< User:0x9f75978>
How to solve it?
create method sessions_controller.rb is:
def create
user = User.find_by_name(params[:name])
if user and user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to admin_url
else
redirect_to login_url, alert: "Invalid user/password combination"
end
end
It's trying to call authenticate on User, this means you probably don't have an authenticate method on user. If you do, make sure it's not private.
The code you give tries to call the authenticate method on an instance of a User object.
Based on you comment re: User.authenticate(name, password) you have an authenticate method only on the User class - and it takes both name and password as parameters.
To call the User class method, you'd instead use the following:
user = User.find_by_name(params[:name])
if user && User.authenticate(params[:name], params[:password])
Alternatively, look for an instance-level method called authenticate which (as #caulfield mentioned above) would look something like:
def authenticate(password)
# does stuff
end
instead of User.authenticate(name,password)
I'm using the facebooker gem which creates a variable called facebook_session in the controller scope (meaning when I can call facebook_session.user.name from the userscontroller section its okay). However when I'm rewriting the full_name function (located in my model) i can't access the facebook_session variable.
You'll have to pass the value into your model at some point, then store it if you need to access it regularly.
Models aren't allowed to pull data from controllers -- it would break things in console view, unit testing and in a few other situations.
The simplest answer is something like this:
class User
attr_accessor :facebook_name
before_create :update_full_name
def calculated_full_name
facebook_name || "not sure"
end
def update_full_name
full_name ||= calculated_full_name
end
end
class UsersController
def create
#user = User.new params[:user]
#user.facebook_name = facebook_session.user.name
#user.save
end
end