I am trying to create a logging feature on my RoR application that logs all the actions performed by the user on a given controller in a model. I tried implementing it with filters but they didn't solve my problem because they didn't allow me to properly log "create" actions.
"Create" actions are tricky because, when the before/after filters are called the action hasn't been saved yet, therefore, I don't have the corresponding id of the model (which I need).
I know I could use "after_commit", but this would greatly increase the complexity of the logging feature, since the "parameters" saved in each log entry are exposed to the controller, but not to the model.
Is there a way to dynamically add an "after_commit" filter to an instance of ActiveRecord?
Read this, I think this is nice solution: Notifications
This is how i log, i have a users controller create action like this:
def create
#user = User.new(params[:user])
if #user.save
flash[:notice] = "Welcome, #{#user.username}"
redirect_to(:controller => "users", :action => "home")
session[:id] = #user.id
else
render("home")
end
end
Now i would like to log that a user was created, then i do this:
First create an AuditLogger class in User.rb(model):
class User < ActiveRecord::Base
...some stuff other....
class AuditLogger < Logger
def format_message(severity, timestamp, progname, msg)
"#{timestamp.to_formatted_s(:db)} #{severity} #{msg}\n"
end
end
Then back to the controller(users.rb)
def create
#user = User.new(params[:user])
if #user.save
logfile = File.open("#{Rails.root}/log/my_log.log", 'a')
audit_log = AuditLogger.new(logfile)
audit_log.info "#{#user.firstname} was created successfully"
redirect_to(:controller => "users", :action => "home")
else
render("home")
end
end
Also you will need to create a file in your log directory called my_log.log. Hopefully it should be able to log. I know its not the best solution and there i are better ways of doing it, but at the time i needed something to work urgently, so i stuck with it.
Checkout these links:
rails logging tips
alternative logging solution
Related
I have a FormObject for registration which creates a user and a lot of models for him inside create method.
def create
ActiveRecord::Base.transaction do
#user = User.create(#params[:user])
process_bonuses_for_user
process_actions_for_user
# et.c., giant amount of methods
end
#user.persisted? # to return the true of false to controller
end
I met the strange behaviour of my FormObject. Even it ran successfull (create a lot of models) or unsuccessful (not saving them), the id of User model is autoincrementing. So, every trying to save something using my FormObject increment the value of next id for User. This is normal situation when User created successfully, but not normal when user makes a mistake on registration form.
How can I disable this unpredictable behaviour?
P.S. I know that everything is work when I write #user = User.new(#params[:user]) at the start of create method and #user.save at the end, but there are a lot of associations, and I don't want to write a lot of autosave or inverse_of in my models.
P.P.S. I'm postgresql-9.4 user
Your transaction is not working because you're using create. You need to use the bang version (create!) to raise an exception on failure which triggers the rollback. Do note that you'll need to rescue the InvalidRecord exception yourself.
In my opinion, it could be something like this:
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
process_bonuses_for_user
process_actions_for_user
# et.c., giant amount of methods
...
format.html { redirect_to ... }
end
end
I'm attempting to manage user sign ups and log in with omniauth. I'd also like to collect more information about users after they authorize with the provider. At the moment Vendor#from_omniauth returns either an existing vendor or a new Vendor record with their auth hashes already populated. Is it possible to pass along the new record to the Vendor controller so the form can easily use it?
I'm using Rails 4.
class SessionsController < ApplicationController
def create
vendor = Vendor.from_omniauth(env["omniauth.auth"])
if vendor.new_record?
redirect_to new_vendor_url(vendor)
else
session[:vendor_id] = vendor.id
redirect_to root_url, notice: "Signed in!"
end
end
end
This issue was resolved by changing new_vendor_url(vendor) to new_vendor_url(vendor: vendor.attributes).
HTTP is stateless, so you'll need to either include info in the params, in the session (via cookies or other), or in the database.
Given that you seem to be doing a registration process, I might be inclined to put the info in the database as soon as you get it. It's an additional write and then read, but if the user drops out midway through the process, you can still contact them (in theory) to resume the signup.
Pass the vendor.id to as an argument to new_vendor_url, then do the lookup in the rendered action:
# app/controllers/sessions_controller.rb
redirect_to new_vendor_url(:vendor_id => vendor.id)
# app/controllers/vendors_controller.rb
def new
vendor_from_session = Vendor.find(params[:vendor_id])
# code here
#vendor = Vendor.new
end
My app allows users to log in and have sessions. I have a user controller and a sessions controller, mainly developed from railscasts "authorization from scratch".
I recently added the ability to upload files to S3 using a jquery uploader... Again lots of this comes from railscasts "uploading to amazon S3".
The problem is my uploads are not user specific. Right now my "upload" controller has an "authorize" before_filter to ensure you must be logged in to access the uploader; however once a user uploads a file, ALL users see the upload! Not good! I need to ensure users only see the respective files they upload.
I've tried a few things but none seem to work. I'm looking for some direction on how to ensure users only see the files they upload. I'm following different railscasts and rails documentation on nesting resources (I think that is how I have to do this?) but I keep missing something as there seems to be lots of changes that I don't 100% understand. I fix one error, then hit another, and am wondering if I'm even going down the right path or maybe I'm missing something?
The way I thought this should work is to first nest the resource:
resources :users do
resources :cust_uploads
end
Then I modified the models as below and ran "rake db:migrate" to tie them together... I may need to manually modify a migration file with a foreign id field?:
class User < ActiveRecord::Base
has_secure_password
attr_accessible :email, :password, :password_confirmation
validates_uniqueness_of :email
has_many :CustUploads
end
class CustUpload < ActiveRecord::Base
attr_accessible :cust_file_url, :name
before_create :default_name
belongs_to :User
def default_name
self.name ||= File.basename(cust_file_url, '.*').titleize if cust_file_url
end
end
This gives me tons of path errors which I'm fighting through now... as my new_cust_upload_path is probably something like new_user_cust_upload_path
I also think my forms and controllers need lots of modification....
I'm using form_for
<%= form_for(#cust_upload) do |f| %>
Which I think should now be #user.cust_upload?
controllers at the moment:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
redirect_to root_url, notice: "Thank you for signing up!"
else
render "new"
end
end
end
class CustUploadsController < ApplicationController
before_filter :authorize
def index
#cust_uploads = CustUpload.all
end
def show
#cust_upload = CustUpload.find(params[:id])
end
def new
#cust_upload = CustUpload.new
end
def create
#cust_upload = CustUpload.create(params[:cust_upload])
end
def edit
#cust_upload = CustUpload.find(params[:id])
end
def update
#cust_upload = CustUpload.find(params[:id])
if #cust_upload.update_attributes(params[:cust_upload])
redirect_to #cust_upload_url, notice: "Cust upload was successfully updated."
else
render :edit
end
end
def destroy
#cust_upload = CustUpload.find(params[:id])
#cust_upload.destroy
redirect_to cust_uploads_url, notice: "Cust Upload was successfully destroyed"
end
end
Any direction will be greatly appreciated. I've been through many tutorials and can make simple things work from scratch, I just can't seem to integrate this functionality with my existing app. There is something here I can't wrap my brain around and I'm hoping someone can provide me with that Eurika moment! Thanks
EDIT
routes.rb and my models appear to have the appropriate connections (code below). When in terminal I type "rake routes" I get a list as expected (see below) however I get and error: "No route matches {:action=>"show", :controller=>"cust_uploads"}" for a link with user_cust_uploads_path. There is a show template in the cust_uploads path and rake routes says it exists! What am I missing?
user_cust_uploads GET /users/:user_id/cust_uploads(.:format) cust_uploads#index
POST /users/:user_id/cust_uploads(.:format) cust_uploads#create
new_user_cust_upload GET /users/:user_id/cust_uploads/new(.:format) cust_uploads#new
edit_user_cust_upload GET /users/:user_id/cust_uploads/:id/edit(.:format) cust_uploads#edit
user_cust_upload GET /users/:user_id/cust_uploads/:id(.:format) cust_uploads#show
Considering you want to achieve
Users who upload stuff should only see them.
Why don't you associate the uploads with a specific user id and then when showing them in the view pull them from their own id (current_user.uploads)
In my Rails app, when creating a business I have a form that has the following field:
<%= check_box_tag(:default_company) %>
<%= label_tag(:default_company, "Set Company as Default") %>
Essentially when I create a business, if they check this box, I need it to run something like the following code:
def set_default_company(company, user)
exists = DefaultCompany.find(user.id)
if exists
exists.update_attributes(company: company)
else
DefaultCompany.create(company: company, user: user)
end
end
While learning, I would usually do that stuff in my controller but i'm trying to follow best practices and use a fat model, skinny controller, so I'm wanting to use logic like this:
def create
#company = Company.new(params[:company])
if #company.save
if params[:default_company]
Company.set_default_company(#company.id, current_user.id,)
end
flash[:notice] = "Company was successfully created."
redirect_to #company
else
redirect_to new_company_path
end
end
Here is where I am getting confused on whether to use a class method or an instance method, to call set_default_company. They both seem like they would work and I can't see a benefit to one or the other.
In addition to giving me any information as to which method to use, if someone could show me a brief implementation of writing that as a class method vs. instance method it may give me a better understanding as to why.
Here is how I would write them:
def self.set_default_company(company, user)
# Logic here
end
def set_default_company(company, user)
# Logic here
end
Writing them that way I don't see a benefit to either.
As their name suggests, instance methods on a model should be used for logic/operations that relate to a specific instance of a user (the one on which the method is called.) So you could think of setting the default company for a user as an instance method on User. Class methods are for things which don't operate on an individual instance of a model or for cases where you don't have the instance available to you. e.g. you might have a class method to tidy up your database such as User.purge_expired_users which would not apply to an individual user object.
e.g.
class User
def set_default_company(company)
exists = DefaultCompany.find(self.id)
if exists
exists.update_attributes(company: company)
else
DefaultCompany.create(company: company, user: self)
end
end
end
then your controller method would look like:
def create
#company = Company.new(params[:company])
if #company.save
if params[:default_company]
current_user.set_default_company #company
end
flash[:notice] = "Company was successfully created."
redirect_to #company
else
redirect_to new_company_path
end
end
Alternatively, you could think of the relationship from the other perspective and put an instance method on Company e.g. company.set_as_default_for(user).
I would actually make set_default_company an instance method on User. A User has a default Company; why should a Company need to what users it is default for?
class User
def set_default_company(company)
exists = DefaultCompany.find(id)
if exists
exists.update_attributes(company: company)
else
DefaultCompany.create(company: company, user: self)
end
end
end
In my opinion, I always create a class method if the method in question represents information/behavior that is quite generic among all the objects instantiated, different from the instance methods, that I use when I believe it's more like a specific action of the instantiated object in question.
But that is my point-of-view.
A few things: do you have a separate table for DefaultCompany? This seems like it should be a boolean flag on the company table.
Next, is there an association between companies and users? If so, it seems the best way to do it would be
In the user model
def set_default_company(company)
self.companies.each do |c|
c.update_attributes(:default => false)
end
company.update_attributes(:default => true)
end
Or in the Company model
def set_as_default
update_attributes(:default_company => true)
end
How can I implement PRG in Rails?
I used PRG in Rails, but I am not totally convinced it's right. I was wondering is there any better way to handle it in Rails?
I don't know how popular PRG pattern is and why one has to religiously stick to the "redirect" on failure aspect of it (actually, one good reason is sometimes you dont want to deal with the "setup" complexity at create failure and keep things dry).
What you basically need is to transfer the params for :user to new. I think #Hitesh's solution above is quite close.
class UsersController < ApplicationController
def new
if flash[:user_params]
#user = User.new(flash[:user_params])
#user.valid?
else
#user = User.new
end
end
def create
#user = User.new(params[:user])
if #user.save
# clears previously stored user if there is any
flash[:notice] = "User created."
redirect_to '/'
else
flash[:error] = "Error saving User"
flash[:user_params] = params[:user]
redirect_to :action => :new
end
end
end
Use the session, Luke
The way you implemented it in your blog post is quite fine, however you may want to use session instead of flash to store your #user and optionally use the ActiveRecord session store to keep cookies from getting bloated.
From ActionController::Base documentation
ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and, unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
config.action_controller.session_store = :active_record_store
in your config/environment.rb and run rake db:sessions:create.
So you should…
class UsersController < ApplicationController
def new
#user = session[:user] || User.new
end
def create
#user = User.new(params[:user])
if #user.save
# clears previously stored user if there is any
session[:user] = nil
redirect_to '/'
else
session[:user] = #user
redirect_to :action => :new
end
end
end
I'm no expert in these matters, but this looks good. From what I understand flash is a part of the session. So the answers telling you to switch to session seem a bit misguided. In this case you want the data to be cleared after the redirect. Other than shoving it in the session, I'm not sure where you would put it.
As far as your cookie size increasing, well, the default session provider for Rails is a cookie in Rails 3. You could swap the session provider out if you wanted to keep the data server side. It is encrypted though, so you are probably okay with the data in the cookie, unless size is an issue.
use below code
class UsersController < ApplicationController
def new
#user = User.new(session[:user_param])
session[:user_param]=nil
end
def create
#user = User.new(params[:user])
if #user.save
# clears previously stored user if there is any
flash.discard(:user)
redirect_to '/'
else
session[:user_param] = #user
redirect_to :action => :new
end
end
end
It is true, though, that you should not do redirect_to '/'. You should define root in your routes file and then do redirect_to root_path.
Edit: Oops, that was supposed to be a comment to SpyrosP's answer.
Also: Here is some excellence guidance on flash. Particularly this may ease your mind:
The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available in the next request, which is useful for storing error messages etc.
The interesting things there is that, yes it is a part of the session, so answers to "use the session instead of flash" are misguided, as Justin Etheredge's answer already put it. The other thing is that it says it is useful for storing messages instead of only for storing messages. With the added "etc" it would lead me to believe that it is within the intended usage to store user information in there as well.
One last thing, I would agree with Aditya Sanghi that you should just store the user parameters and not an entire user object in the flash.
I didn't read the question properly.
The validation failure you have necessitates going to a different page where a different process will occur. You tried to update a domain object, it doesn't exist. The usual response to a validation failure is to re-render the page, but you need to go to the create page.
The flash hash seems wrong for this. I'd agree with the idea of stuffing your entered data into the session and redirecting.