I have a Log model that belongs to User and Firm. For setting this I have this code in the logs_controller's create action.
def create
#log = Log.new(params[:log])
#log.user = current_user
#log.firm = current_firm
#log.save
end
current_user and current_firm are helper methods from the application_helper.rb
While this works it makes the controller fat. How can I move this to the model?
I believe this sort of functionality belongs in a 'worker' class in lib/. My action method might look like
def create
#log = LogWorker.create(params[:log], current_user, current_firm)
end
And then I'd have a module in lib/log_worker.rb like
module LogWorker
extend self
def create(params, user, firm)
log = Log.new(params)
log.user = user
log.firm = firm
log.save
end
end
This is a simplified example; I typically namespace everything, so my method might actually be in MyApp::Log::Manager.create(...)
No difference: You can refactor the code:
def create
#log = Log.new(params[:log].merge(:user => current_user, :firm => current_firm)
#log.save
end
And your Log have to:
attr_accessible :user, :firm
Not much shorter, but the responsibility for the handling of current_user falls to the controller in MVC
def create
#log = Log.create(params[:log].merge(
:user => current_user,
:firm => current_firm))
end
EDIT
If you don't mind violating MVC a bit, here's a way to do it:
# application_controller.rb
before_filter :set_current
def set_current
User.current = current_user
Firm.current = current_firm
end
# app/models/user.rb
cattr_accessor :current
# app/models/firm.rb
cattr_accessor :current
# app/models/log.rb
before_save :set_current
def set_current
self.firm = Firm.current
self.user = User.current
end
# app/controllers/log_controller.rb
def create
#log = Log.create(params[:log])
end
Related
So i'm working on a sort of custom-rolled history tracking for a RoR application. The part i'm hung up on is getting the logged in users information to tie to the record. I've figured out getting the user, its by a submodule which is attached to the ActionController::Base class. The problem is, I'm having trouble retrieving it from the submodule.
Here is my code:
module Trackable
# This is the submodule
module TrackableExtension
extend ActiveSupport::Concern
attr_accessor :user
included do
before_filter :get_user
end
def get_user
#user ||= current_user # if I log this, it is indeed a User object
end
end
# Automatically call track changes when
# a model is saved
extend ActiveSupport::Concern
included do
after_update :track_changes
after_destroy :track_destroy
after_create :track_create
has_many :lead_histories, :as => :historical
end
### ---------------------------------------------------------------
### Tracking Methods
def track_changes
self.changes.keys.each do |key|
next if %w(created_at updated_at id).include?(key)
history = LeadHistory.new
history.changed_column_name = key
history.previous_value = self.changes[key][0]
history.new_value = self.changes[key][1]
history.historical_type = self.class.to_s
history.historical_id = self.id
history.task_committed = change_task_committed(history)
history.lead = self.lead
# Here is where are trying to access that user.
# #user is nil, how can I fix that??
history.user = #user
history.save
end
end
In my models then its as simple as:
class Lead < ActiveRecord::Base
include Trackable
# other stuff
end
I got this to work by setting a Trackable module variable.
In my TrackableExtension::get_user method I do the following:
def get_user
::Trackable._user = current_user #current_user is the ActionController::Base method I have implemented
end
Then for the Trackable module I added:
class << self
def _user
#_user
end
def _user=(user)
#_user = user
end
end
Then, in any Trackable method I can do a Trackable::_user and it gets the value properly.
I'm using Devise and Rails 3.2.16. I want to automatically insert who created a record and who updated a record. So I have something like this in models:
before_create :insert_created_by
before_update :insert_updated_by
private
def insert_created_by
self.created_by_id = current_user.id
end
def insert_updated_by
self.updated_by_id = current_user.id
end
Problem is that I get the error undefined local variable or method 'current_user' because current_user is not visible in a callback. How can I automatically insert who created and updated this record?
If there's an easy way to do it in Rails 4.x I'll make the migration.
Editing #HarsHarl's answer would probably have made more sense since this answer is very much similar.
With the Thread.current[:current_user] approach, you would have to make this call to set the User for every request. You've said that you don't like the idea of setting a variable for every single request that is only used so seldom; you could chose to use skip_before_filter to skip setting the User or instead of placing the before_filter in the ApplicationController set it in the controllers where you need the current_user.
A modular approach would be to move the setting of created_by_id and updated_by_id to a concern and include it in models you need to use.
Auditable module:
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
# Assigns created_by_id and updated_by_id upon included Class initialization
after_initialize :add_created_by_and_updated_by
# Updates updated_by_id for the current instance
after_save :update_updated_by
end
private
def add_created_by_and_updated_by
self.created_by_id ||= User.current.id if User.current
self.updated_by_id ||= User.current.id if User.current
end
# Updates current instance's updated_by_id if current_user is not nil and is not destroyed.
def update_updated_by
self.updated_by_id = User.current.id if User.current and not destroyed?
end
end
User Model:
#app/models/user.rb
class User < ActiveRecord::Base
...
def self.current=(user)
Thread.current[:current_user] = user
end
def self.current
Thread.current[:current_user]
end
...
end
Application Controller:
#app/controllers/application_controller
class ApplicationController < ActionController::Base
...
before_filter :authenticate_user!, :set_current_user
private
def set_current_user
User.current = current_user
end
end
Example Usage: Include auditable module in one of the models:
# app/models/foo.rb
class Foo < ActiveRecord::Base
include Auditable
...
end
Including Auditable concern in Foo model will assign created_by_id and updated_by_id to Foo's instance upon initialization so you have these attributes to use right after initialization, and they are persisted into the foos table on an after_save callback.
another approach is this
class User
class << self
def current_user=(user)
Thread.current[:current_user] = user
end
def current_user
Thread.current[:current_user]
end
end
end
class ApplicationController
before_filter :set_current_user
def set_current_user
User.current_user = current_user
end
end
current_user is not accessible from within model files in Rails, only controllers, views and helpers. Although , through class variable you can achieve that but this is not good approach so for that you can create two methods inside his model. When create action call from controller then send current user and field name to that model ex:
Contoller code
def create
your code goes here and after save then write
#model_instance.insert_created_by(current_user)
end
and in model write this method
def self.insert_created_by(user)
update_attributes(created_by_id: user.id)
end
same for other methods
just create an attribute accessor in the model and initialize it when your record is being saved in controller as below
# app/models/foo.rb
class Foo < ActiveRecord::Base
attr_accessor :current_user
before_create :insert_created_by
before_update :insert_updated_by
private
def insert_created_by
self.created_by_id = current_user.id
end
def insert_updated_by
self.updated_by_id = current_user.id
end
end
# app/controllers/foos_controller.rb
class FoosController < ApplicationController
def create
#foo = Foo.new(....)
#foo.current_user = current_user
#foo.save
end
end
A user can sign up as an artist. All the user needs to do now, is provide his email.
In Artist controller, def create. Is it normal to have something like:
def create
#artist = current_user
respond_to do |format|
if #artist.update_attributes(params[:user]) # params[:user] contains email
#artist.is_artist = true
#artist.save
....
In my User model, I have:
attr_accessible :email
Which means, I can't simply do #artist.update_attributes(:is_artist => true). I would have to use the save method instead. Is this type of approach common? Or is there a better way?
You can define before_create method in your model:
class User < ActiveRecord::Base
...
before_create :fill_fields
def fill_fields
is_artist = true
end
end
I would do the following:
1st: I wound not set up an ArtistController if you do not have an Artist Model. rather I would add a non-restful method in your UserController, and push the implemention logic into the model ...
# config/routes.rb
resources :users do
member {post 'signup_as_artist'}
end
# UserController
def signup_as_artist
#user = User.find(params[:id])
#user.signup_as_artist
end
# User
def signup_as_artist
self.update_attribute :is_artist, true
end
Good luck
I have 3 tables
items (columns are: name , type)
history(columns are: date, username, item_id)
user(username, password)
When a user say "ABC" logs in and creates a new item, a history record gets created with the following after_create filter.
How to assign this username ‘ABC’ to the username field in history table through this filter.
class Item < ActiveRecord::Base
has_many :histories
after_create :update_history
def update_history
histories.create(:date=>Time.now, username=> ?)
end
end
My login method in session_controller
def login
if request.post?
user=User.authenticate(params[:username])
if user
session[:user_id] =user.id
redirect_to( :action=>'home')
flash[:message] = "Successfully logged in "
else
flash[:notice] = "Incorrect user/password combination"
redirect_to(:action=>"login")
end
end
end
I am not using any authentication plugin. I would appreciate if someone could tell me how to achieve this without using plugin(like userstamp etc.) if possible.
Rails 5
Declare a module
module Current
thread_mattr_accessor :user
end
Assign the current user
class ApplicationController < ActionController::Base
around_action :set_current_user
def set_current_user
Current.user = current_user
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end
end
Now you can refer to the current user as Current.user
Documentation about thread_mattr_accessor
Rails 3,4
It is not a common practice to access the current_user within a model. That being said, here is a solution:
class User < ActiveRecord::Base
def self.current
Thread.current[:current_user]
end
def self.current=(usr)
Thread.current[:current_user] = usr
end
end
Set the current_user attribute in a around_filter of ApplicationController.
class ApplicationController < ActionController::Base
around_filter :set_current_user
def set_current_user
User.current = User.find_by_id(session[:user_id])
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
User.current = nil
end
end
Set the current_user after successful authentication:
def login
if User.current=User.authenticate(params[:username], params[:password])
session[:user_id] = User.current.id
flash[:message] = "Successfully logged in "
redirect_to( :action=>'home')
else
flash[:notice] = "Incorrect user/password combination"
redirect_to(:action=>"login")
end
end
Finally, refer to the current_user in update_history of Item.
class Item < ActiveRecord::Base
has_many :histories
after_create :update_history
def update_history
histories.create(:date=>Time.now, :username=> User.current.username)
end
end
The Controller should tell the model instance
Working with the database is the model's job. Handling web requests, including knowing the user for the current request, is the controller's job.
Therefore, if a model instance needs to know the current user, a controller should tell it.
def create
#item = Item.new
#item.current_user = current_user # or whatever your controller method is
...
end
This assumes that Item has an attr_accessor for current_user.
The Rails 5.2 approach for having global access to the user and other attributes is CurrentAttributes.
If the user creates an item, shouldn't the item have a belongs_to :user clause? This would allow you in your after_update to do
History.create :username => self.user.username
You could write an around_filter in ApplicationController
around_filter :apply_scope
def apply_scope
Document.where(:user_id => current_user.id).scoping do
yield
end
This can be done easily in few steps by implementing Thread.
Step 1:
class User < ApplicationRecord
def self.current
Thread.current[:user]
end
def self.current=(user)
Thread.current[:user] = user
end
end
Step 2:
class ApplicationController < ActionController::Base
before_filter :set_current_user
def set_current_user
User.current = current_user
end
end
Now you can easily get current user as User.current
The Thread trick isn't threadsafe, ironically.
My solution was to walk the stack backwards looking for a frame that responds to current_user. If none is found it returns nil. Example:
def find_current_user
(1..Kernel.caller.length).each do |n|
RubyVM::DebugInspector.open do |i|
current_user = eval "current_user rescue nil", i.frame_binding(n)
return current_user unless current_user.nil?
end
end
return nil
end
It could be made more robust by confirming the expected return type, and possibly by confirming owner of the frame is a type of controller...
Within Authlogic, is there a way that I can add conditions to the authentication method? I know by using the find_by_login_method I can specify another method to use, but when I use this I need to pass another parameter since the find_by_login_method method only passes the parameter that is deemed the 'login_field'.
What I need to do is check something that is an association of the authentic model.. Here is the method I want to use
# make sure that the user has access to the subdomain that they are
# attempting to login to, subdomains are company names
def self.find_by_email_and_company(email, company)
user = User.find_by_email(email)
companies = []
user.brands.each do |b|
companies << b.company.id
end
user && companies.include?(company)
end
But this fails due to the fact that only one parameter is sent to the find_by_email_and_company method.
The company is actually the subdomain, so in order to get it here I am just placing it in a hidden field in the form (only way I could think to get it to the model)
Is there a method I can override somehow..?
Using the answer below I came up with the following that worked:
User Model (User.rb)
def self.find_by_email_within_company(email)
# find the user
user = self.find_by_email(email)
# no need to continue if the email address is invalid
return false if user.nil?
# collect the subdomains the provided user has access to
company_subdomains = user.brands.map(&:company).map(&:subdomain)
# verify that the user has access to the current subdomain
company_subdomains.include?(Thread.current[:current_subdomain]) && user
end
Application Controller
before_filter :set_subdomain
private
def set_subdomain
# helper that retreives the current subdomain
get_company
Thread.current[:current_subdomain] = #company.subdomain
end
User Session Model (UserSession.rb)
find_by_login_method :find_by_email_within_company
I have read a few things about using Thread.current, and conflicting namespaces.. This is a great solution that worked for me but would love to hear any other suggestions before the bounty expires, otherwise, +100 to Jens Fahnenbruck :)
Authlogic provides API for dealing with sub domain based authentication.
class User < ActiveRecord::Base
has_many :brands
has_many :companies, :through => :brands
acts_as_authentic
end
class Brand < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :brands
has_many :users, :through => :brands
authenticates_many :user_sessions, :scope_cookies => true
end
Session controller:
class UserSessionsController < ApplicationController
def create
#company = Company.find(params[:user_session][:company])
#user_session = #company.user_sessions.new(params[:user_session])
if #user_session.save
else
end
end
end
On the other hand
Here is a way to solve the problem using your current approach(I would use the first approach):
Set custom data - to the key email of the hash used to create the UserSession object.
AuthLogic will pass this value to find_by_login method. In the find_by_login method access the needed values.
Assumption:
The sub domain id is set in a field called company in the form.
class UserSessionsController < ApplicationController
def create
attrs = params[:user_session].dup #make a copy
attrs[:email] = params[:user_session] # set custom data to :email key
#user_session = UserSession.new(attrs)
if #user_session.save
else
end
end
end
Model code
Your code for finding the user with the given email and subdomain can be simplified and optimized as follows:
class User < ActiveRecord::Base
def find_by_email params={}
# If invoked in the normal fashion then ..
return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)
User.first(:joins => [:brands => :company}],
:conditions => ["users.email = ? AND companies.id = ?",
params[:email], params[:company]])
end
end
Edit 1
Once the user is authenticated, system should provide access to authorized data.
If you maintain data for all the domains in the same table, then you have to scope the data by subdomain and authenticated user.
Lets say you have Post model with company_id and user_id columns. When a user logs in you want to show user's posts for the sub domain. This is one way to scope user's data for the subdomain:
Posts.find_by_company_id_and_user_id(current_company, current_user)
Posts.for_company_and_user(current_company, current_user) # named scope
If you do not scope the data, you will have potential security holes in your system.
In your lib folder add a file with the follwing content:
class Class
def thread_local_accessor name, options = {}
m = Module.new
m.module_eval do
class_variable_set :"###{name}", Hash.new {|h,k| h[k] = options[:default] }
end
m.module_eval %{
FINALIZER = lambda {|id| ###{name}.delete id }
def #{name}
###{name}[Thread.current.object_id]
end
def #{name}=(val)
ObjectSpace.define_finalizer Thread.current, FINALIZER unless ###{name}.has_key? Thread.current.object_id
###{name}[Thread.current.object_id] = val
end
}
class_eval do
include m
extend m
end
end
end
I found this here
Then add code in the controller like this:
class ApplicationController < ActionController
before_filter :set_subdomain
private
def set_subdomain
User.subdomain = request.subdomains[0]
end
end
And now you can do the following in your user model (assuming your company model has a method called subdomain:
class User < ActiveRecord::Base
thread_local_accessor :subdomain, :default => nil
def self.find_by_email_within_company(email)
self.find_by_email(email)
company_subdomains = user.brands.map(&:company).map(&:subdomain)
company_subdomains.include?(self.subdomain) && user
end
end
And FYI:
companies = user.brands.map(&:company).map(&:subdomain)
is the same as
companies = []
user.brands.each do |b|
companies << b.company.subdomain
end
With rails 3 you can use this workaround:
class UserSessionsController < ApplicationController
...
def create
#company = <# YourMethodToGetIt #>
session_hash = params[:user_session].dup
session_hash[:username] = { :login => params[:user_session][:username], :company => #company }
#user_session = UserSession.new(session_hash)
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default dashboard_url
else
#user_session.username = params[:user_session][:username]
render :action => :new
end
...
end
Then
class UserSession < Authlogic::Session::Base
find_by_login_method :find_by_custom_login
end
and
class User < ActiveRecord::Base
...
def self.find_by_custom_login(hash)
if hash.is_a? Hash
return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
find_by_email_and_company_id(hash[:login], hash[:company].id)
else
raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
end
end
...
end
Which is quite plain and "correct". I take me a lot of time to find out, but it works fine!