I am going through the great Michael Hartl tutorial to build ruby app here.
I am trying to understand the concept of how to create a session and I am stuck in understanding this line:
self.current_user = user
in this method:
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
end
I understand the whole concept of creating a cookie with the user_token.
But I don't understand what does self.current_user = user means and why is it even necessary to keep this line of code - I have the cookie with the token - why do I need to know the current user?
Also, where does this "self" is being stored - it is not like a flash[:success] parameter I can see in one of my views. so I don't understand where it is.
there are also these 2 methods in the same module:
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
And still I am trying to connect the dots of the purpose for this mysterious current user - is its purpose is to create #current_user global variable to use in the views?
If so - why there are there these 2 duplicated functions def current_user=(user) and def current_user
A few things.
First, you're reading the method names wrong (which is not surprising given how cryptic ruby method naming can be). def current_user=(user) is actually read as defining the method current_user= that takes an argument user, whereas def current_user defines a method current_user that takes no arguments. These are referred to respectively as setters and getters.
Here's a reference: Ruby (programming language): What are setters and getters in Ruby?
So that explains the duplication. On to your next question.
I don't understand what does self.current_user = user means
self is a topic unto itself, worthy of its own discussion, so I won't even try to explain it (here's one reference out of many). For the purposes of this question it's just important to remember that in order to set instance variables, you need to prefix your assignment with self, even within the class (where for other purposes it would be implicit). The rest of the line is a call to the current_user= setter method I mentioned above, with the argument user.
why is it even necessary to keep this line of code - I have the cookie with the token - why do I need to know the current user?
The reason it's necessary is that you don't want to be looking up the user from the token every time you need to get the current user. Take a look at the getter method:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
What this says is: if I haven't looked up and set the instance variable #current_user yet, then look it up; if I have already set it, then just return it. That saves a lot of looking up.
I think that answers your questions. There are a lot of deeper issues (self, etc.) which you can find more information about elsewhere. Here's one discussion of why you need to include self in setters on SO: Why do Ruby setters need "self." qualification within the class?
UPDATE: Small clarification, that last link about using self for setters within the class is actually a bit off-topic, since you're calling it within a module and not directly from a class. In the context of a module, the self in self.current_user = user will become the class that the module is included inside of, e.g. User.current_user if it was called within the class User, etc. Again, another topic of discussion unto itself...
The method def current_user=(user) is basically a setter that the sign_in method uses in order to set the current_user.
def current_user will return the #current_user or if it is not set it will find it in the Users table by the remember_token. This basically allows you get the current_user at any point in time.
self.current_user in the context of the sign_in method will refer to the calling class or module in this case. It will be calling current_user from the Session Helper module.
Related
I'm new to Ruby and Ruby on Rails, coming from a background of C-like languages.
Here is some code that I found in the application_controller.rb file:
def current_user
#current_user ||= Renter.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
def authorize_user
redirect_to '/login' unless current_user
end
Here is what I don't understand about it:
- On line 4, is :current_user invoking the current_user instance method, or directly accessing the #current_user instance variable?
- On line 7, is current_user invoking the current_user instance method, or directly accessing the #current_user instance variable?
- On line 2, is :user_id a variable or is it more like a string literal being used as a key? Kind of like in JavaScript one might write session["user_id"] to get theuser_id property of the session object.
class methods aren't relevant in this example - they aren't being used here.
Instance variables will never call methods when they are get/set.
Although the opposite does sometimes happen. It's a very common pattern to create getter/setter methods for instance variables, so common that attr reader/writer/accessor helpers are defined in ruby core. If you write attr_accessor :foo, then foo= and foo will get/set the instance variable.
But this does not happen by default.
To answer your other question:
A symbol :user_id starts with a colon and is similar to a string. The difference between a symbol and a string may seem arbitrary, but it is an important concept in Ruby and making the distinction in your head is a good idea.
To respond to your comment:
Line 4, helper_method :current_user is really something specific to rails, consider it "rails magic" if you like. In effect this is making your current_user method callable from views (whereas by default it would only be available in the controller). :current_user is a symbol which is used to reference the current_user method. Not necessarily something you have to understand in total detail, it would suffice to know that helper_method takes a symbol with the same name as a method and makes that method available to views. As far as I'm aware, it's only relevant to Rails controllers.
It's somewhat common in Ruby to use symbols that refer to method names. It's a more intermediate concept. You can see another example in send:
def asd
return 0
end
class Foo
def instance_method_bar
return 0
end
def self.class_method_bar
return 0
end
end
# how the methods are typically called
asd
Foo.new.instance_method_bar
Foo.class_method_bar
# another way to call them, using send
send(:asd)
Foo.new.send(:instance_method_bar)
Foo.send(:class_method_bar)
I'm not recommending you use send unless you need to, but hopefully it will make it more clear how the symbol :current_user is being used in helper_method
Line 7 is the current_user method being called.
Let's tackle your questions one at a time.
On line 4, :current_user is a method, most likely used to return the current_user, so you can access username, or email, or whatever value the user has.
On line 7, it is still the same method. In this case, Ruby is checking whether a current_user object exists. You can think of unless as if not. So the code will redirect to login if current_user is false, which will happen if current_user == nil. If the user is logged in, current_user != nil, and the redirect does not happen.
On line 2, :user_id is a symbol, and session is a hash, which is key-value pair, such as { a: 1, b: 2 }, and you access the value with the key using the [] method, so session[:user_id] is returning the value of the user_id. In Ruby, you can use anything as the key, symbols are used because they are always unique, the object id of :two and :two is the same, whereas the id is different for "two" and "two".
I'm following railstutorial by Michael Hartl. In chapter 8.2.2 he defines a variable #current_user and a method current_user.
app/helpers/sessions_helper.rb looks like this:
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
end
Hartl defines #current_user an instance variable (of User object I guess); How can #current_user be an instance variable if it is itself an instance of the User class?
This is such a good question. It seems that this arrangement is not something that it was planned for, it simply happened: u need to have a variable that has some kind of memory outside the method (scope), in order to compare if #current_user=#current_user, but at the same time the whole arrangement makes theoretically no sense.
This article was written in 2008 and was proof read from members of the rails core team. This paragraph is very telling of the situation:
http://www.railway.at/articles/2008/09/20/a-guide-to-memoization/
--> "A little note on naming here: Some people seem to prefer prefixing the memoizing variable’s name with an underscore to indicate that it’s not meant to be used as an actual instance variable. To be honest, I don’t think this is really necessary unless you define a whole bunch of instance variables and memoized variables."
The SessionsHelper module is mixed into your controllers, so #current_user will be set as an instance variable of the controller which is handling the current request (Rails creates a new controller instance to handle each request)
So I have many places in my program where I use the: #user = User.find(params[:id])
Now, I wanted to make a new method in ApplicationController so that all my controllers would be able to use the method so that I would not have to repeat my self so much.
def find_user(params[:id])
#user = User.find(params[:id])
end
So now when I want to display users in some controller, I just type the find_user(params[:id]) in an action. But this doesn't seem to work for some reason.
First I'd like to say that although it's a common call, I would not make a method for it, because it is already essentially just calling one method, but here:
You're making it a little too complicated:
In the application controller
def find_user(user_id)
#user = User.find(user_id)
end
In the controller you're using it in
find_user(params[:id])
Alternatively if for some reason you don't want to write params everytime
In the application controller
def find_user(paramsicle) # params might be reserved
#user = User.find(paramsicle[:id])
end
In the controller you're using it in
find_user(params)
EDIT: It'd probably be helpful if I explained why yours doesn't work...
Yours is fine, except that method arguments (the stuff in the parenthesis) should just be a identifier except in special cases (optional arguments, the *args things). Read more about it here. The problem wasn't with params, but with trying to access id while still in the arguments section. That's why it was find to call paramsicle[:id] afterwards.
I'd like to use pg_audit_log for logging in a rails app. The audit log must not only show the columns that have changed, but also the user who made those changes. The docs don't show how to do this, but after looking through the pg_audit_log source (postgresql_adapter.rb) I see it reads user information from a thread local variable, ala:
current_user = Thread.current[:current_user]
I've considered setting/unsetting this in before and after filters like so:
Thread.current[:current_user] = current_user
(using the current_user helper method in the controller to get the currently logged in user), but that seems dangerous. I'm now spending time trying to understand how the rails request cycle and threads interact, to get a better feel for just how dangerous. In the mean time, I was curious if any SO users currently using pg_audit_log have solved the need to log the user_id and user_unique_name to the log tables each time the user makes a change to a record.
Setting the current user the way you describe is a common way to do it. See, for example, http://rails-bestpractices.com/posts/47-fetch-current-user-in-models
Some example code could look like:
# in your model
class User < ActiveRecord::Base
def self.current
Thread.current[:current_user]
end
def self.current=(user)
Thread.current[:current_user] = user if user.nil? || user.is_a?(User)
end
end
# in your controller
class ApplicationController < ActionController::Base
before_filter :set_current_user
def set_current_user
User.current = user_signed_in? ? current_user : nil
end
end
Relying on the Thread.current hash to provide model-level access to objects managed by the controller is indeed controversial. For example, see the following:
Safety of Thread.current[] usage in rails
It is worrisome that this particular feature is undocumented in the pg_audit_log gem.
Suppose you had not actively explored the gem's source code, and suppose you had independently decided to define Thread.current[:current_user] = something in your own application, for your own purpose. In that case, pg_audit_log would audit that object, without your knowledge.
Granted, the name current_user is so universally accepted to mean the currently logged-on user as defined by authentication routines that it's difficult to imagine this potential bug as a concrete problem, but from a design standpoint? Ouch.
On the other hand, since you know what you are doing, ensuring that Thread.current[:current_user] is set/unset at the beginning/end of each and every response cycle should make the process safe. At least that's what I gather from reading lots of posts on the topic.
Cheers, Giuseppe
everybody! Recently, I am working on Michael Hartle's RoR tutorial. In chapter 8, I encounter one problem which has confused me for two days. Here is the problem. In section 8.2.3.
module SessionsHelper
def sign_in(user)
.
.
.
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user # Useless! Don't use this line.
end
end
And Michael writes:
If we did this, we would effectively replicate the functionality of
attr_accessor, which we saw in Section 4.4.5.5 The problem is that it
utterly fails to solve our problem: with the code in Listing 8.21, the
user’s signin status would be forgotten: as soon as the user went to
another page—poof!—the session would end and the user would be
automatically signed out. To avoid this problem, we can find the user
corresponding to the remember token created by the code in Listing
8.19, as shown in Listing 8.22.
The Listing 8.22.
module SessionsHelper
.
.
.
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
end
My questions are: Why the previous code would make the session log out automatically when user go to a new page? Why the second piece of code wouldn't? I think, as long as a user log in, the #current_user's value will always be "user" until he log out explicitly, right?
The previous code doesn't so much log the user out, as doesn't re-create the user on subsequent requests.
State is not shared across requests, and has to be re-created with every request. #current_user is an instance variable, and keeps it's value for the duration of a single request.
To get around the fact that state is not shared, with each request we need to reload necessary variables such as #current_user from something that is common across the session, in this case they're using the remember_token cookie.
The first snippet of code does not reload #current_user on each request, so will forget what value it held as soon as the user browses to another page after logging in, the second snippet attempts to load the current user via the remember_token cookie, so after this has been set when someone logs in, should remember the user until that cookie expires.
What he is saying here is that when we use #current_user in the first example when not on a sign in page, we have not called #current_user = User.find(1). We are relying on it already having been set. Since we are not explicitly setting #current_user the following:
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
Says if the #current_user is not set, then set it by finding the user using the id stored in the cookie. That way when we navigate to a page where we do not explicitly set the #current_user it will have been populated.