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.
Related
I have a user registration with an extra field called "company_name". After the user gets created, I want a Company instance to be created based on the extra field "company_name" and that user associated with the company. I've tried a few things like this:
class RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
super
company = Company.create(name: params[:company_name])
current_user.admin = true
current_user.company = company
current_user.save
end
def update
super
end
end
however, I don't have a current_user when trying to do the lines after I create the company. Is there a better way of doing this?
You can pass a block to the Devise controller's create that will give you the created user resource:
class RegistrationsController < Devise::RegistrationsController
CREATE_COMPANY_PARAMS = [:name]
def create
super do |created_user|
if created_user.id
company = Company.create! create_company_params
created_user.update! company_id: company.id
end
end
end
private
def create_company_params
params.require(:user).require(:company).permit(*CREATE_COMPANY_PARAMS)
end
end
There are some tough parts to handling this correctly though.
It seems that even if the user already exists, it will still call your block and pass you a user, but the user won't have an id assigned because the DB save failed. The if created_user.id check prevents a company from being created for an invalid user.
If the company already exists. The .create! will throw an exception which causes the controller to return an HTTP 422.
Utilizing the after_save callback in User model is probably suitable for this case:
# app/models/user.rb
class User < ActiveRecord::Base
...
# Execute this callback after an record is saved only on create
after_save :create_and_associate_company, on: :create
private:
def create_and_associate_company
company = self.companies.build
# Other necessary attributes assignments
company.save
end
end
Reference on other Active Record Callbacks.
You can access the newly created user using the resource variable
Here, I'm logging info only if the user was actually saved
class RegistrationsController < Devise::RegistrationsController
def create
super
if resource.persisted?
Rails.logger.info("Just created and saved #{resource}");
end
end
end
Here is my code so far:
class Video < ActiveRecord::Base
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
if: Proc.new { |v| v.needs_approval? }
validate :should_be_saved?
def should_be_saved?
errors.add(:base, 'added for approval') if needs_approval?
end
def needs_approval
#needs_approval ||= false
end
def needs_approval?
#needs_approval
end
end
# ApplicationController
class ApplicationController < ActionController::Base
def user_for_paper_trail
return unless user_signed_in?
original_user.present? ? original_user : current_user
end
# Used to determine the contributor
# When contributor logs in, warden saves the contributor_id in session
def original_user
return nil unless remember_contributor_id?
#original_user ||= User.find(remember_contributor_id)
end
def info_for_paper_trail
{ athlete_id: current_user.id } if current_user
end
end
The problem I am running into currently is when the Video object is saved the validation fails (because I told it too), but I need for the validation to fail but the version object continue with its creation. Just not too sure how to go about doing that.
EDITS
Here is my code (the code below is still using the ApplicationController code from above):
class Video < ActiveRecord::Base
# .. other methods
include Contributable
attr_accessible :video_type_id, :athlete_id, :uploader_id, :created_at, :updated_at, :uniform_number, :featured,
:name, :panda_id, :date, :thumbnail_url, :mp4_video_url, :from_mobile_device, :duration, :sport_id,
:delted_at, :approved
end
module Contributable
extend ActiveSupport::Concern
included do
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
unless: Proc.new { |obj| obj.approved? },
skip: [:approved]
end
def log_changes_or_update(params, contributor = nil)
update_attribute(:approved, false) unless contributor.blank?
if contributor.blank?
update_attributes params
else
self.attributes = params
self.send(:record_update)
self.versions.map(&:save)
end
end
end
class VideosController < ApplicationController
def update
# ... other code
# original_user is the contributor currently logged in
#video.log_changes_or_update(params[:video], original_user)
end
end
The app I am working on has a small layer of complexity that allows for users with a certain role to edit profiles they have access too. I am trying to save the versions of each change out (using paper_trail) without affecting the existing object.
The code above works exactly how I want it to, however, I am just curious to know if in my log_changes_or_update method is not the correct way to go about accomplishing the overall goal.
Why not just remove the validation and add an approved attribute with a default value of false to the Video model? That way the Video object is saved and a paper_trail version is created. Later when the video gets approved paper_trail will note that change too.
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
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
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...