Edit - I misunderstood the exact nature of the problem, have reposted here with a better grasp of the issue: Rails / Devise - updating session variables between controller actions
I am using a session variable to pass data between three form submission controller actions in my Rails app, but I am having an issue where the process works the first time I run through but fails the second time because the session variable does not pass to a subsequent controller action - however, if I clear my browser history, it works fine (but will fail if I try to repeat without clearing history, etc.). The app uses Devise for user log in, and the problem seems to be related to the session being destroyed on log out.
The session variable in question here is session[:ids], and it seems that it is not being passed from the submission action to the approval action without first clearing the browser history.
Here are the routes:
post 'report/submission'
get 'report/approval'
get 'report/summary' => 'report#summary'
And the associated controller actions (added a few groups of puts to check the variable values in the console):
def submission
#incomingReport = ActiveSupport::JSON.decode(params[:report])
#newReportIDArray = Array.new
#incomingReport.each do |x|
#DATA PROCESSING STUFF HERE
#newReportIDArray.push(#new_report.id)
end
session[:ids] = #newReportIDArray
puts "submission_marker"
puts #newReportIDArray
puts "submission_marker_end"
respond_to do |format|
format.json do
render json: {
success: 200
}.to_json
end
end
end
#USER LOGIN OCCURS HERE
def approval
#reportIDs = session[:ids]
puts "approval_marker"
puts #reportIDs
puts "approval_marker_end"
#reportIDs.each do |x|
#new_report = Report.find(x)
#new_report.user_id = current_user.id
#new_report.save
end
redirect_to report_summary_path
end
def summary
#reportIDs = session[:ids]
puts "summary_marker"
puts #reportIDs
puts "summary_marker_end"
end
#USER LOGS OUT AFTER SUMMARY
So if I run this with clean history, my console looks like this:
submission_marker
766
submission_marker_end
approval_marker
766
approval_marker_end
summary_marker
766
summary_marker_end
but if I log out after the first summary and then I try to run through the process without clearing the history, I end up with
submission_marker
767
submission_marker_end
approval_marker
approval_marker_end
and the error undefined method 'each' for nil:NilClass, referring to #reportIDs.each in the approval action.
What am I missing here? Why is it that with a clean history, everything runs ok, but with a 'dirty' history, the session variable sets for the submission action but is dropped by/on its way to the approval action? I see that this has something to do with Devise and the destruction of the session, but I don't understand why I can't create a new session variable.
Related
I have an ActiveRecord model named Document and have implemented CRUD operations around it. I just have a problem with persisting a Document instance between requests when validation fails (be cause I wanna redirect to another page when this happens).
First, I tried storing the instance in the flash session:
# documents_controller.rb
def new
#document = flash[:document] || Document.new
end
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
With the code above, I was expecting that the actual Document instance was stored in the flash session, but instead it became a string which looks somewhat like #<Document:0xad32368>. After searching online for a while, I found out that for some reasons you cannot store ActiveRecord objects in sessions.
There are a lot of suggestions about just storing the object's id in the flash session, but I can't do that because as you can see, the object is not yet stored in the database.
Next, I tried reconstructing the Document instance after the redirect, taking advantage of the instance's attributes method (which returns a serializeable hash that can be stored in the session):
# documents_controller.rb
def new
#document = Document.new(flash[:document_hash] || {})
end
def create
...
flash[:document_attributes] = document.attributes
redirect_to new_document_path
end
This almost solved the problem, except for the part in which the validation errors (document.errors) are not preserved. Also, if this is used to persist an instance already stored in the database (in the case of failed validations when updating a Document instance), I'm not sure which between the original attributes and the new attributes will get persisted.
Right now I've already run out ideas to try. Anyone who has a decent solution for this?
EDIT:
You might be wondering why I still have to redirect to another page instead of just rendering the new document view template or the new action in the create method. I did so because there are some things in my views that are dependent on the current controller method. For example, I have a tab which needs to be highlighted when you are on the document creation page (done by checking if action_name == "new" and controller_name == "documents"). If I do:
def create
...
render action: "new"
end
the tab will not get highlighted because action_name will now be create. I also can't just add additional condition to highlight the tab if action_name == "create" because documents can also be created from the the index page (documents_path). Documents can also be updated from the index page (documents_path) or from the detail page (document_path(document)), and if validation fails in the update method, I'd like to redirect to the previous page.
If I really need to fake persisting something between requests (all of the variables that you set are lost between requests), I will ususally put the relevant attributes into hidden fields in the new form.
In your case, this is overkill. In your code, you are redirecting, which causes a new request:
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
You can easily render the output of another action, instead of redirecting, by using render action: 'action_to_render'. So in your example, this would probably be:
def create
#document = Document.new(document_params)
if #document.save
render action: 'index'
else
render action: 'new'
end
end
Which can be simplified to:
def create
#document = Document.new(document_params)
action_to_render = #document.save ? 'index' : 'new'
render action_to_render
end
If you need extra logic from the action, you can refactor the logic to a method called from both actions, or simply call the other action from the current one.
It is fine once in a while, but I would caution that having to jerk around with the rendering too much is usually indicative of poor architecture.
Edit:
An additional option, given the newly highlighted constraints, could be to make the new and create methods the same. Remove the new action and routes, and make create answer for GET and PATCH requests. The action might look something like:
def create
#document = Document.new(document_params)
request.patch? && #document.save && redirect_to( documents_path )
end
I actually use something very similar to this for almost all of my controllers, as it tends to DRY things significantly (as you can remove the extra probably identical view, as well)
Another option would be to just use an instance variable to keep track of the active tab in this instance, and make the rest of the code a lot cleaner.
SOLVED
I was able to make a workaround for it using ActiveSupport::Cache::Store (as suggested by #AntiFun). First I created a fake_flash method which acts closely like the flash sessions except that it uses the cache to store the data, and it looks like this:
def fake_flash(key, value)
if value
Rails.cache.write key, value
else
object = Rails.cache.read key
Rails.cache.delete key
object
end
end
And then I just used it like the flash session.
# documents_controller.rb
def new
...
#document = fake_flash[:document] || Document.new
...
end
def create
document = Document.new document_params
...
# if validation fails
fake_flash :document, document
redirect_to new_document_page
end
The original, happily working version of my project looked like this:
1) User fills out form (new action) and hits submit (create action)
2) User is redirected to their edit page (edit action uses an edit_id created by model, not Rails auto-gen id), which shows the info user had already submitted
3) User can choose to change info (update action) and re-submit
In this version, even if the user changes nothing in the edit page and submits, the page will still flash a success alert.
From a database perspective, I don’t really care, because since the form is prefilled with the user’s info, the update_attributes method is just overriding old info with the same info.
From a user perspective though, it's annoying, so I want to ensure that the info is only updated and the success alert flashed only if the user actually changes something.
I thought this would be really easy, changing the old code from this:
def update
#request = Request.find_by_edit_id(params[:edit_id])
if #request.update_attributes(request_params)
flash[:success] = true
redirect_to edit_request_path(#request.edit_id)
else
render 'edit'
end
end
And adding one additional component to the "if" like this:
def update
#request = Request.find_by_edit_id(params[:edit_id])
if #request.update_attributes(request_params) && #request.changed?
flash[:success] = true
redirect_to edit_request_path(#request.edit_id)
else
render 'edit'
end
end
But this doesn’t work. Now what happens is that, on the edit page, if I don’t change any info and hit submit, nothing happens (which is great), but if I DO change info and hit submit, still nothing happens (which is bad). What am I doing wrong?
Note: I initially thought it was an order of operations error, so I made it a nested if, with first if #request.update_attributes, and second if #request.changed, but this didn't work either...
The update_attributes method includes the 'save' call as part of its method and is returning true if the save is successful. I think you're looking for something like this using assign_attributes:
def update
#request = Request.find_by_edit_id(params[:edit_id])
#request.assign_attributes(request_params)
if #request.changed?
if #request.save
flash[:success] = true
redirect_to edit_request_path(#request.edit_id)
else
render 'edit'
end
else
# Action if no change has been made
end
end
The correct custom params are being displayed in my debug function after the form is submitted but the default params are displayed when I enter console.
Controller
def update
current_user.update_attributes(params[:user])
flash[:success] = "Your settings have been saved!"
render new_status_update_path
end
Model
attr_accessible :deficit_pct,
:target_bf_pct,
:activity_factor
Notes:
The closest question I could find to this on SO is a question that changes the attributes of an object that exists through an association.
I've tried using the Object.update method although I get an error that says:
private method `update' called for #
Any ideas?
Thanks!
Try the code :-
def update
if current_user.update_attributes(params[:user])
flash[:success] = "Your settings have been saved!"
render new_status_update_path
else
p 111111111111
p current_user.errors.inspect
end
end
after check your log for any errors.exist for that active record
After playing around with in the console I've found out that even if I change the attributes manually the attributes don't 'stick' after I exit the console.
So I'll enter console, change the users attributes, test them, and they'll be changed. If I exist and re-enter, THEN test them, they'll have reverted back to their default values.
This leads me to believe that the 'after_initialize' method within the user model which sets its default values is running after each save. I though that it would only run after the object had been saved for the first time alone but now I know it run each time it is saved.
after_initialize :default_values
def default_values
self.goal = "Cut"
self.measurement = "US"
self.bmr_formula = "katch"
self.fat_factor = 0.655
self.protein_factor = 1.25
self.deficit_pct = 0.10
self.target_bf_pct = 0.10
self.activity_factor = 1.3
end
Once I remove all these values and the after_initialize method, it saves permanently.
You should make sure that you don't have any validation errors. You can check them using:
active_record_model.errors
In your case, it would be
current_user.errors
Also, you should print the return value from update_attributes to see if it's true or false. If you get false, the save was cancelled. This was most likely caused by validation errors or a callback returning false.
Something like:
def update
if current_user.update_attributes(params[:user])
flash[:success] = "Your settings have been saved!"
render new_status_update_path
else
some_error_handling_code
end
end
Would not display success when the save fails. As a general rule, you should check whether a save, or any other back end operation, fails, before reporting success to the end user.
I am installing Kissmetrics on my rails app by storing events in a session variable and then passing them into the kissmetrics javascript code on the subsequent page. This method works great except for trying to track accounts getting created. It seems that when I store the account created event in my session variable it works fine, but by the time the next page loads, the session variable is gone. I put debugger in there to try to find where it is getting deleted but it seems there's nothing. km_log_event is a method that stores the string in a session variable called km_events. Here's my code:
accounts_controller/create -->
...
if #account.save
log_event("Account", "Created", #account.name)
km_log_event("Account Created")
redirect_to(welcome_url(:subdomain => #account.subdomain))
#user.activate!
#user.add_connection(params[:connect_to])
else
render(:action => 'new', :layout => 'signup')
end
...
sessions_controller/welcome -->
def welcome
if current_account.new?
# Create the session for the owner, the account is brand new
current_account.user_sessions.create(current_account.owner, true)
elsif current_account.users.last && current_account.users.last.created_at > 1.hour.ago
current_account.user_sessions.create(current_account.users.last, true)
end
redirect_to embedded_invitations_path
end
I'm just not sure where it is getting deleted so I can't record this event. It seems to be happening after #account.save in the accounts controller but before the welcome action.
UPDATE:
here is the accounts module where I believe (this isn't my codebase) current_account gets defined.
module Accounts
def self.included(controller)
controller.helper_method :current_account
end
protected
def current_account
return #current_account if defined?(#current_account)
#current_account = Account.find_by_subdomain!(current_subdomain)
end
end
An invalid csrf token will have the session be reset. Could this be happening?
You can test this easily by removing the following from your controller (usually in ApplicationController)
protect_from_forgery
I think this is happening as you are trying to share session between subdomains. To achieve this you have to do some configuration.
Refer Subdomain Session Not Working in Rails 2.3 and Rails 3 on Heroku with/without a Custom Domain?
In a project which uses restful_authentication with acts_as_state_machine and email activation, I get a double render error whenever a user does the activation action from the email link.
I'm using the default
def activate
self.current_user = params[:activation_code].blank? ? false : User.find_by_activation_code(params[:activation_code])
if logged_in? && !current_user.active?
current_user.activate!
flash[:notice] = "Signup complete!"
end
redirect_back_or_default('/')
end
to activate, and the default
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
to redirect. The redirect method works in every other case it's called in the same way.
The double render error occurs at the render of the page main_page/home that is routed as "/".
What should I be looking for?
Acts As State Machine will sometimes have some odd behavior where the saved record written to the database will be out of sync with the object in memory. I bet you have a situation where the ruby object corresponding to the newly activated user is not being updated even though the field in the db is being set (of vice versa).
I'd need to see the controller action that actually runs to render the route you have setup to match "/", but I bet you've got subtly inconsistent cases in that action that are being tripped up by this inconsistency in AASM. Try reloading the user object at the start of that controller action to see if the problem goes away. If not begin debugging by making sure that your state changes are actually being saved out to the db.