I have a Rails 3 app that I am trying to implement devise and declarative_authorization.
An important part of declarative_authorization is the presence of a function "role_symbols" within the user model.
Because of the way I implement roles, I am implementing an instance method within the User model to keep track of a value (let's call foo) as such:
attr_accessor :foo
def foo=(val)
#foo = val
end
def foo
#foo
end
Then we will use the value of foo inside the role_symbols method to limit the valid roles, maybe like this:
def role_symbols
roles.where("foo = ?", #foo).name.underscore.to_sym
end
The issue is when I try to set the value of foo for the current_user in a controller, the value doesn't stick, for example:
current_user.foo = 99
is successful, but when I check the value in another view (or controller), the value of current_user.foo is nil.
Isn't the current_user object just a User object that is persisted in the Session?
If so, is there some lock on setting instance values within the current_user object?
Was lucky enough to find the answer here:
https://web.archive.org/web/20140113211231/http://blog.drivingthevortex.nl/2010/01/24/using-declarative_authorization-with-subdomains/
Related
My application has a Group object, of which a user has many. The navbar displays which group is currently selected, and the objects displayed on various pages are based on this. A number of my models have 'group_id' fields, and I'd like for these fields to be populated with the id of the currently selected group when they're saved.
In my application controller I have a helper_method which returns the current_group however this can't and shouldn't be accessed from a model which is the DRYest way I could think of doing this.
#inhereted_model.rb
before_save :assign_group_reference
def assign_group_reference
self.group_id = current_group.id
end
Is there an efficient and DRY way to do this that I'm missing?
You are right; any controller helper-methods cannot and should not be accessed directly from a model method.
I think a standard DRY way is to set the parameter of a model in your Controller methods. For example, do as follows in your Controller(s):
# In a Controller
def my_helper(mymodel)
mymodel.group_id = current_group
# where current_group is your Controller helper method to obtain the group name.
end
def create # or update etc.
#mymodel = set_my_model # your arbitrary method to set a model
my_helper(#mymodel)
respond_to do |format|
if #mymodel.save
format.html { redirect_to #mymodel, notice: 'Success.' }
else
raise
end
end
end
If you want, you can write my_helper (which in this case takes no argument and sets the instance variable #mymodel instead of a local variable) in before_action in combination with only or except, where you make sure the method is called after a model #mymodel is set, in order to avoid calling my_helper repeatedly in many methods in the Controller.
Alternatively, if you really want to set it at a model level for some reason, a potential workaround is to use a Ruby Thread variable, like the following.
# In a controller
def create
model = set_my_model # Arbitrary routine to set a model
Thread.new{
Thread.current.thread_variable_set(:grp, current_group)
# where current_group is your Controller helper method to obtain the group name.
model.save!
# In the model, you define a before_save callback
# in which you write something like
# self.group_id = Thread.current.thread_variable_get(:grp)
}.join
end
But I think this is a little dirty hack and I would avoid it in principle.
In my application controller I have the following code:
helper_method :current_user
def current_user
#current_user ||= User.find(session[:user]) if session[:user]
end
I would like to use the "current_user" method in a model. According to the rails API the helper method can be accessed at "AbstractController::Helpers::ClassMethods".
See link:
http://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html
When I add this to my model I get a method not found error:
include AbstractController::Helpers::ClassMethods
Am I missing something in how to include these helper methods?
Thanks!
Short answer: you can't and you shouldn't.
The model doesn't have (and should not have) any visibility of the view and the controller. The clear separation is one of the key principle of the MVC pattern.
If you want a method in your model to have access to the current user, then pass the user when invoking the method.
For instance, assuming you want to pass the user on the Post creation, define a custom method
class Post
def do_something_with_user(user)
# ...
end
end
and call it from the controller
def action
Post.find(...).do_something_with_user(current_user)
end
There are possible workarounds, such as storing the current user into the current thread or in a global variable, but this is gonna break the rules (and you should not break the rules).
In custom validation methods, why are the attributes passed as local variables instead of being accessible as instance variables?
I was expecting to use #title instead of title in the custom validation below, but #title is nil in the code below. title contains the actual data.
attr_accessible :title
validate :do_check_title
def do_check_title
title =~ /^Alice in/ || errors.add(:title, "Not Alice in Wonderland")
end
Looking through the active_record/core.rb
def initialize(attributes = nil)
...
assign_attributes(attributes) if attributes
...
end
And then in active_record/attribute_assignment.rb
def _assign_attribute(k, v)
public_send("#{k}=", v)
So I guess, the attributes should be available as instance variables in the validation function.
Why are they nil?
It doesn't really matter if you access these from a validation or other instance methods. You can see what's happening from the console (pry):
u = User.first
show-method u.first_name=
This gives you something like this:
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
Now if you take a look at the write_attribute method, you can see that it deletes attribute cache etc. and then assign to attributes and it's just a hash.
u.attributes
So from now on, instead of the boring u.first_name = "foo", you can use this:
u.send :write_attribute, "first_name", "foo"
and it will do the same thing (3.2.10).
ActiveRecord stores the values in the attributes hash and not in instance variables. I generates accessor (getter/setter) methods on the fly while you access them.
Rails needs to support the livecycle of an entity, and wraps this code in the generated methods. This is needed, so that it can support ie. dirty? and changed? methods.
The next thing are associations, they are handled through proxies, so you need to call them with methods too.
Instance variables could be seen as volatile data. They are transparent to the persistance layer.
I've created an instance variable on an ActiveRecord Model where I want to save a bit of computationally heavy data in each instance... Here's my code to do that:
class Account < ActiveRecord::Base
after_initialize :init
attr_accessor :market_value
def init
self.market_value ||= my_lengthy_function
end
end
where I'll take the hit to get that instance data (market_value) run when I init an instance of the model.
This works - I can see how I don't have to re-calculate my market_value property.
My problem is, when I access that object through another context, rails doesn't leverage that data as I'd expect.
For example:
Say I create an instance of an account (a = Account.find_by_id(2)). That market_value will be calculated on that object once.
If I have a nested has_many relationship to something called "holdings" (not in my sample code) on that account object, I'm going to want each of those holding objects (a holding) to be able to use it's parent account object.
However, in my code, I access the account from it's nested holding objects (my_holding.account.market_value) - I re-instantiate an instance of that account object, and incur that costly computation, even though it's already been computed.
How can I better leverage that account market_value property so that it doesn't keep recalculating?
I would not put the calculation logic in the ActiveRecord model. Maybe something along these lines:
class MarketValueCalculator
def initialize()
#market_values = {}
end
def calculate_for_account(account)
#market_values[account.id] ||= heavy_lifting
end
def heavy_lifting
###
end
end
#calculator = MarketValueCalculator.new
#market_value = #calculator.calculate_for_account(account)
#market_value = #calculator.calculate_for_account(my_holding.account)
i would build up a simple cache on class-level with the model ids as keys:
class Account < ActiveRecord::Base
def market_value
##market_value ||= {}
##market_value[id] ||= my_lengthy_function
end
end
not tested if this would work though, especially with class reloading in development.
Oddly enough, most of this works as it has been written, however I'm not sure how I can evaluate if the current_user has a badge, (all the relationships are proper, I am only having trouble with my methods in my class (which should partially be moved into a lib or something), regardless, the issue is specifically 1) checking if the current user has a record, and 2) if not create the corresponding new record.
If there is an easier or better way to do this, please share. The following is what I have:
# Recipe Controller
class RecipesController < ApplicationController
def create
# do something
if #recipe.save
current_user.check_if_badges_earned(current_user)
end
end
So as for this, it definitely seems messy, I'd like for it to be just check_if_badges_earned and not have to pass the current_user into the method, but may need to because it might not always be the current user initiating this method.
# User model
class User < ActiveRecord::Base
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1, user)
end
if user.recipes.count > 20
award_badge(2, user)
end
end
def award_badge(badge_id, user)
#see if user already has this badge, if not, give it to them!
unless user.badgings.any? { |b| b[:badge_id] == badge_id}
#badging = Badging.new(:badge_id => badge_id, :user_id => user)
#badging.save
end
end
end
So while the first method (check_if_badges_earned) seems to excucte fine and only give run award_badge() when the conditions are met, the issue happens in the award_badge() method itself the expression unless user.badgings.any? { |b| b[:badge_id] == badge_id} always evaluates as true, so the user is given the badge even if it already had the same one (by badge_id), secondly the issue is that it always saves the user_id as 1.
Any ideas on how to go about debugging this would be awesome!
Regardless of whether you need the current_user behavior above, award_badge should just be a regular instance method acting on self instead of acting on the passed user argument (same goes for check_if_badges_earned). In your award_badge method, try find_or_create_by_... instead of the logic you currently have. For example, try this:
class User < ActiveRecord::Base
# ...
def award_badge(badge_id)
badgings.find_or_create_by_badge_id(badge_id)
end
end
To access the current_user in your model classes, I sometimes like to use thread-local variables. It certainly blurs the separation of MVC, but sometimes this kind of coupling is just necessary in an application.
In your ApplicationController, store the current_user in a thread-local variable:
class ApplicationController < ActionController::Base
before_filter :set_thread_locals
private
# Store thread-local variables so models can access them (Hackish, but useful)
def set_thread_locals
Thread.current[:current_user] = current_user
end
end
Add a new class method to your ActiveRecord model to return the current_user (you could also extend ActiveRecord::Base to make this available to all models):
class User < ActiveRecord::Base
def self.current_user
Thread.current[:current_user]
end
end
Then, you'll be able to access the current user in the instance methods of your User model with self.class.current_user.
What you need to do first of all is make those methods class methods (call on self), which avoids needlessly passing the user reference.
Then, in your award_badge method, you should add the Badging to the user's list of Badgings, e.g.: user.badgings << Badging.new(:badge_id => badge_id)