Rails page caching and flash messages - ruby-on-rails

I'm pretty sure I can page cache the vast majority of my site but the one thing preventing me from doing so is that my flash messages will not show, or they'll show at the wrong time.
One thing I'm considering is writing the flash message to a cookie, reading it and displaying it via javascript and clearing the cookie once the message has been displayed.
Has anyone had any success doing this or are there better methods?
Thanks.

Cacheable flash do this:
in your application controller:
after_filter :write_flash_to_cookie
def write_flash_to_cookie
cookie_flash = cookies['flash'] ? JSON.parse(cookies['flash']) : {}
flash.each do |key, value|
if cookie_flash[key.to_s].blank?
cookie_flash[key.to_s] = value
else
cookie_flash[key.to_s] << "<br/>#{value}"
end
end
cookies['flash'] = cookie_flash.to_json
flash.clear
end
and then read "flash" cookie via Javascript and insert the message inside the HTML

I'm dealing with the same problem and I found cacheable-flash plugin that does exactly what KJF described in the question.
I think this is simpler and nicer solution than making excessive ajax calls.

One solution would be to cache the page, but include a javascript snippet that will make another small request just for the section you want to be dynamic. So the user will download the page fully, and then when javascript executes, it will pull down the dynamic page element.
I wrote a short blog post about this a while back.
http://chase.ratchetsoftware.com/2008/12/rails-caching-dynamic-fragments/
Also, Greg Pollack of RailsEnvy did a screencast where he focuses on having dynamic data in cached pages.
http://railslab.newrelic.com/2009/02/05/episode-5-advanced-page-caching
Hope this helps,
Chase Gray

You don't have to cache entire page. Try fragment caching API

Old question... but I got around this by including the flash message into my cache key.
caches_action :show, cache_path: proc { |c|
most_recent_update_time = MyClass.order('updated_at DESC').limit(1).first.try(:updated_at).to_i
{ tag: most_recent_update_time.to_s + c.flash.collect{|x| x}.join }
}
If you have flash messages on your show action this will obviously break the cache often, but works well if you aren't doing a lot of messages.

Unobtrusive Flash puts flash message into cookie, and displays it via JavaScript. It provides vanilla and Bootstrap flavored JS display logics. It works in normal and ajax requests. It is also easy to hook into frameworks such as AngularJS.

I don't use Rails but this is how I did it in Python using UUIDs:
# set flash messages like this
def flash(self, title, body):
session['flash_messages'].append({
'title': title,
'body': body,
'uuid': uuid().hex # stores a UUID as a string
})
...
self.flash('foo', 'bar')
Then in the base template I have this:
<script type="text/javascript">
{% for m in session.flash_messages %}
if(!Cookies.get('{{m.uuid}}')) {
Notify('{{m.title}}', '{{m.body}}');
Cookie.set('{{m.uuid}}', 'true', 86400); // key, value, expiry seconds
}
{% endfor %}
</script>
I'll break it down for the Pythonically-challenged:
When you add a flash message, you create a unique ID and store it with that message.
Before you display the message, you check to see if a cookie named with the message's unique ID has been set.
If that cookie has not been set, flash the message and set the cookie. Expire the cookie in a day, or as brief as you think is wise.
Now if this page is pulled from cache, it will be okay. At step 2 the test for the cookie will pass because it has already been set, and the message will not be displayed.

Related

How can we circumvent these remote forms drawback?

In an effort to have everything translateable in our website ( including the error messages for the validations ), we switched almost all of our forms to remote forms. While this helps with the ability to translate error messages, we have encountered other problems, like:
if the user clicks on the submit button multiple times, the action gets called multiple times. If we have a remote form for creating a new record in the database, and assuming that the user's data is valid, each click will add a new object ( with the exact same contents ). Is there any way of making sure that such things cannot happen?
Is there somewhere I could read about remote forms best practices? How could I handle the multiple clicks problem? Is switching all the forms to remote forms a very big mistake?
There is a rails 3 option called :disable_with. Put this on input elements to disable and re-label them while a remote form is being submitted. It adds a data-disable-with tag to those inputs and rails.js can select and bind this functionality.
submit_tag "Complete sale", :disable_with => "Please wait..."
More info can be found here
Easy, and you can achieve that in many ways depending your preferences:
Post the form manually simply using an ajax request and while you wait for the response disable/hide (or whatever you need) the form to ensure the user can't keep doing posts as crazy. Once you get the response from the server, again you can allow the user to post again (cleaning the form first), or show something else or redirect it to another page or again whatever you need.
Use link_to :remote=>true to submit the form and add a callback function to handle the response and also to disable/hide (or whatever you need) the form when it's submitted
Add a js listener to the form to detect when it's submitted and then disable/hide/whatever the form
As you see, there are lots of different ways to achieve what you need.
EDIT: If you need info about binding or handling a form submit from js here you'll find very easy and interesting examples that may help you to do what I suggested you! jQuery Submit
I have remote forms extensively myself, and in most cases I would avoid them. But sometimes your layout or UX demands for on-the-fly drop-down forms, without reloading or refreshing the complete page.
So, let me tackle this in steps.
1. Preventing Normal form double-post
Even with a normal form, a user could double-click your button, or click multiple times, if the user does not get a clear indication that the click has been registered and the action has started.
There are a lot of ways (e.g. javascript) to make this visible, but the easiest in rails is this:
= f.button :submit, :disable_with => "Please wait..."
This will disable the button after the first click, clearly indicating the click has been registered and the action has started.
2. Handling the remote form
For a remote form it is not that much different, but the difference most likely is: what happens afterward ?
With a remote form you have a few options:
In case of error: you update the form with the errors.
you leave the form open, allowing users to keep on entering the data (I think this is your case?)
you redirect the users to some place.
Let me handle those cases. Please understand that those three cases are completely standard when doing a normal form. But not when doing a remote call.
2.1 In case of error
For a remote form to update correctly, you have to do a bit more magic. Not a lot, but a bit.
When using haml, you would have a view called edit.js.haml which would look something like
:plain
$('#your-form-id').replaceWith('#{j render(:partial => '_form') }');
What this does: replace the complete haml with only the form. You will have to structure your views accordingly, to make this work. That is not hard, but just necessary.
2.2 Clearing the form
You have two options:
* re-render the form completely, as with the errors. Only make sure you render the form from a new element, not the just posted one!!
* just send the following javascript instead:
$('#your-form-id').reset();
This will blank the form, and normally, that would effectively render any following clicking useless (some client validation could block posting until some fields are filled in).
2.3 Redirecting
Since you are using a remote form, you can't just redirect. This has to happen client-side, so that is a tad more complicated.
Using haml again this would be something like
:plain
document.location.href = '#{#redirect_uri}';
Conclusion
To prevent double (triple, quadruple, more) posts using remote forms you will have to
disable the button after first click (use :disable_with)
clear the form after succesful submission (reset the form or render with a new element)
Hope this helps.
The simplest solution would be to generate a token for each form. Then your create action could make sure it hasn't been used yet and determine whether the record should be created.
Here's how I would go about writing this feature. Note that I haven't actually tested this, but the concept should work.
1.
Inside the new action create a hash to identify the form request.
def new
#product = Product.new
#form_token = session["form_token"] = SecureRandom.hex(15)
end
2.
Add a hidden field to the form that stores the form token. This will be captured in the create action to make sure the form hasn't been submitted before.
<%= hidden_field_tag :form_token, #form_token %>
3.
In the create action you can make sure the form token matches between the session and params variables. This will give you a chance to see if this is the first or second submission.
def create
# delete the form token if it matches
if session[:form_token] == params[:form_token]
session[:form_token] = nil
else
# if it doesn't match then check if a record was created recently
product = Product.where('created_at > ?', 3.minutes.ago).where(title: params[:product][:title]).last
# if the product exists then show it
# or just return because it is a remote form
redirect_to product and return if product.present?
end
# normal create action here ...
end
Update: What I have described above has a name, it is called a Synchronizer (or Déjà vu) Token. As described in this article, is a proper method to prevent a double submit.
This strategy addresses the problem of duplicate form submissions. A synchronizer token is set in a user's session and included with each form returned to the client. When that form is submitted, the synchronizer token in the form is compared to the synchronizer token in the session. The tokens should match the first time the form is submitted. If the tokens do not match, then the form submission may be disallowed and an error returned to the user. Token mismatch may occur when the user submits a form, then clicks the Back button in the browser and attempts to resubmit the same form.
On the other hand, if the two token values match, then we are confident that the flow of control is exactly as expected. At this point, the token value in the session is modified to a new value and the form submission is accepted.
I hate to say it, but it sounds like you've come up with a cure that's worse than the disease.
Why not use i18n for translations? That certainly would be the 'Rails way'...
If you must continue down this route, you are going to have to start using Javascript. Remote forms are usually for small 'AJAXy things' like votes or comments. Creating whole objects without leaving the page is useful for when people might want to create lots of them in a row (the exact problem you're trying to solve).
As soon as you start using AJAX, you have to deal with the fact that you'll have to get into doing some JS. It's client-side stuff and therefore not Rail's speciality.
If you feel that you've gone so far down this road that you can't turn back, I would suggest that the AJAX response should at least reset the form. This would then stop people creating the same thing more than once by mistake.
From a UI/UX point of view, it should also bring up a flash message letting users know that they successfully created the object.
So in summary - if you can afford the time, git reset and start using i18n, if you can't, make the ajax callback reset the form and set a flash message.
Edit: it just occurred to me that you could even get the AJAX to redirect the page for you (but you'd have to handle the flash messages yourself). However, using a remote form that then redirects via javascript is FUGLY...
I've had similar issues with using a popup on mouseover, and not wanting to queue several requests. To get more control, you might find it easier to use javascript/coffeescript directly instead of UJS (as I did).
The way I resolved it was assigning the Ajax call to a variable and checking if the variable was assigned. In my situation, I'd abort the ajax call, but you would probably want to return from the function and set the variable to null once the ajax call is completed successfully.
This coffeescript example is from my popup which uses a "GET", but in theory it should be the same for a "POST" or "PUT".
e.g.
jQuery ->
ajaxCall = null
$("#popupContent").html " "
$("#popup").live "mouseover", ->
if ajaxCall
return
ajaxCall = $.ajax(
type: "GET"
url: "/whatever_url"
beforeSend: ->
$("#popupContent").prepend "<p class=\"loading-text\">Loading..please wait...</p>"
success: (data) ->
$("#popupContent").empty().append(data)
complete: ->
$"(.loading-text").remove()
ajaxCall = null
)
I've left out my mouseout, and timer handling for brevity.
You can try something like that for ajax requests.
Set block variable true for ajax requests
before_filter :xhr_blocker
def xhr_blocker
if request.xhr?
if session[:xhr_blocker]
respond_to do |format|
format.json, status: :unprocessable_entity
end
else
session[:xhr_blocker] = true
end
end
end
Clear xhr_blocker variable with an after filter method
after_filter :clear_xhr_blocker
def clear_xhr_blocker
session[:xhr_blocker] = nil
end
I would bind to ajax:complete, (or ajax:success and ajax:error) to redirect or update the DOM to remove/change the form as necessary when the request is complete.

How to pass in client-side data when rendering server-side logic via AJAX in Rails 3.1?

This is the problem I'm having. I have these filters and they each have their own url which are generated by a helper method filter_url(filter_name). I want to update these filter urls asynchronously (AJAX) when the url changes (which also happens asynchronously and is pushed with HTML5's History API). However, I can't figure out how to update these filter urls because I need to know the name of the filter I am generating the url for (which is client-side in the data-name DOM attribute) but filter_url is server-side. A sure-fire solution would be to move the filter_url logic client-side, but then I would have to maintain both client-side and server-side logic for handling filter urls which ain't DRY at all. So with that said, is what I'm trying to do possible or am I approaching it the wrong way? Thank you a thousand honey bunches of oats!
Perhaps SO should add some gist type of functionality for source code where you can split it by files. Maybe this might have better readability: https://gist.github.com/4c91435aefde9ad5846f. But I will also paste my code here in case my gist expires.
_filters.html.haml
%ul#filters
- #filters.each do |filter|
%li
%a{:"data-name" => filter.name, :href => filter_url(#url_filters.add_to_url(filter.name))}
filters.js.coffee
$('#filters li a').live 'click', ->
history.pushState(null, "", #href) # This changes the url (not using hash bangs)
$.getScript(#href) # This will call index.js.coffee
filters_controller.rb
class FiltersController < ApplicationController
def index
#url_filters = URLFilters.parse(request.fullpath)
end
end
index.html.haml
= javascript_include_tag :filters
= render "filters"
index.js.coffee
$('#filters li a').each ->
# This is the part I'm having trouble with. The code below obviously won't work but hopefully it'll give you an idea of what I'm trying to do
# But basically I want to update each url filter url to account for the new url when the url changes
$(this).attr('href', '<%= #url_filters.add_to_url($(this).data("name")) %>')
Alright, so I think I found a somewhat elegant solution to this.
I changed index.js.coffee to
# Render the partial and get a DOM fragme
filters = $('<%= escape_javascript(render("filters")) %>')
# Replace all filter urls from the ones in the DOM fragment
$('a.filter-url', filters).each ->
$('#' + #id).attr('href', #href) # Replace the href by selecting the id which is unique
and then also changing the partial _filters.js.coffee to
%ul#filters
- #filters.each do |filter|
%li
%a{:id => "#{filter.name}-#{filter.value}", :href => filter_url(#url_filters.add_to_url(filter.name))}
So what I'm doing now is rendering the filters partial and creating a DOM fragment from that and using jQuery to select all the filter urls within that DOM fragment. Then, I am replacing the DOM fragment urls with the ones in the current view. Seems to work well, but I'm open to any other ideas! Hopefully this will help others who run into a similar scenario.

Combining unobtrusive JavaScript with Pusher

I'm using Pusher to add real-time page updates to my Rails app.
Here's a brief synopsis of how Pusher works (and later I'll show you what I'd like it to do):
Controller:
class ThingsController < ApplicationController
def create
#thing = Thing.new(params[:thing])
if #thing.save
Pusher['things'].trigger('thing-create', #thing.attributes)
end
end
end
Javascript (in <head>...</head>):
var pusher = new Pusher('API_KEY');
var myChannel = pusher.subscribe('MY_CHANNEL');
myChannel.bind('thing-create', function(thing) {
alert('A thing was created: ' + thing.name); // I want to replace this line
});
I want to replace the commented line with an ajax request to trigger some unobtrusive JavaScript. Assume I have the file app/views/things/index.js.erb:
$("things").update("<%= escape_javascript(render(#things))%>");
What should I write in the myChannel.bind callback to trigger the above code to execute?
You are not really comparing apples-to-apples here.
You are comparing the template rendered in an XHR request to things/index to the attributes of the #thing object from a POST to things/create.
You need to process the thing object that gets returned from Pusher in the web browser and modify the DOM accordingly.
Or, an easier solution would probably be to have your controller send formatted HTML to Pusher instead of object attributes. Then your javascript code could just insert the formatted HTML instead of trying to parse the thing object and modify the DOM manually.
Response to #user94154's comment:
DISCLAIMER: I never heard of Pusher until your question.
This does create a challenge b/c typically your HTML is formatted in the view, but you have to send data to Pusher from the controller. I can think of a few ways to do this:
If it isn't much HTML markup, you might want to break the "Don't Repeat Yourself" rule and repeat the HTML markup in your controller and send that to Pusher
If it is a lot of markup, I would abstract my view generation code to helpers. You can call a helper from your controller, too.
On the client side, you should have an empty div (or some DOM element) that can hold the HTML from Pusher. And do something like this:
myChannel.bind('thing-create', function(thing) {
$("div#thing_holder").html(thing.html);
});
http://blog.new-bamboo.co.uk/2010/5/12/integrating-pusher-into-a-complex-app-in-one-day
A little over my head, but I think it may put you on the right track.
pjax might be what you are looking for. Included in the README is how to get started with it in Rails.
I understand, here is how you do it...
Tried writing it on here but the code indenting, etc doesn't work well for longer stuff...
http://jbeas.com/ruby-on-rails/rails-3-pusher-app-and-unobtrusive-js/

Ruby: get address of the redirect when posting a page using Restclient

Hi I was wondering if this is possible, this is my scenario, I have a stand alone file that tries to get information from pages using RestClient and Nokogiri
I need to get the information of all the videos available on a page "http://www.koldcast.tv/" so far I havent found a way to get these results on a HTML page (no flash) other than tricking the search page into returning the whole list back using 3 underscores as the search keywords, the problem is that the search is doing a post to a page which I assume is then redirecting to the final page and it gives you something like this "http://www.koldcast.tv/index.php/landingpage/search_results/921c6b6e491005d91d117b0fa88f31d1/" the problem there is that this url is only alive for 5 or 10 minutes so I cannot use this url everytime i need to run the stand alone file the search is in a form with a post to "http://www.koldcast.tv/index.php" which I imagine takes all the data from that form (there is some other hidden fields) and then redirects to that results page is there a way I can do the post with all the data and then get the page that is being redirected
I thank you for taking your time into helping, if I am not explaining myself complete I'll be happy to clear any doubts thanks a lot!
As that isn't a REST interface, RestClient may not be your best choice. You probably want something that more closely emulates a browser. For example, using mechanize:
require 'mechanize'
a = Mechanize.new
a.get('http://www.koldcast.tv') do |page|
search_result = page.form_with(:action =>"http://www.koldcast.tv/index.php") do |search|
search.keywords = "___"
end.submit
# Print all relative links (starting with "/")
search_result.links_with(:href => /^\//).each do |link|
puts link.href
end
end
This gets you partway there. You can see all the video links.

Prevent Flash-Message showing up twice (on page with error and on next page)

If there is an error in a form my grails application this results in a flash message. If I then go to another page, the (old) flash message shows up again on the new page.
How do I prevent this?
try using request.message, it act the same way as flash.message but will only stay on for on long enough to be displayed once.
controller:
request.message = message;
and on gps you use it like you wold with flash.message:
<g:if test="${request.message }"><br>
<div class="message">${request.message}</div> </g:if>
I would say if the message is for request only, use request.message. If a redirect could be involved, use flash and then clear the flash message after displaying it in the gsp:
<div class="message">
${flash?.message}
</div>
<%
// Prevent flash messages displaying twice if there is no redirect
flash.message = null
%>
I would have all this in a standard template that you use for displaying messages.
One thought would be to clear the flash message on the first page when an error is detected.
Not sure if the request.message route is still an option in the newer version of grails as I tried this and it didn't work for me.
A method I found to avoid showing the message twice is to set a message using a more specific key in flash such as:
Controller:
flash.specificKeyForControllerAndAction = "Some message"
GSP:
<g:if test="${flash.specificKeyForControllerAndAction}">
<div class="message">${flash.specificKeyForControllerAndAction}</div>
</g:if>
Obviously the key could be anything you would like, but make sure your other views aren't checking for the same key or else the message will display again.
quote from grails documentation
http://docs.grails.org/3.1.1/ref/Controllers/flash.html
The flash object is a Map (a hash) which you can use to store key value pairs. These values are transparently stored inside the session and then cleared at the end of the next request.
This pattern lets you use HTTP redirects (which is useful for redirect after post) and retain values that can be retrieved from the flash object.
You must remember how works redirect http://grails.asia/grails-redirect-vs-forward
When you do a redirect, there is a go back to the client browser, and the browser call the url it received, then this call is the "next" request after adding flash message.
Then you should add a flash message before a redirect. Because this flash message is cleared at the end of the next request. Or if you do not use a redirect, and just do a simple forward (a simple return render of your gsp for example) there is no other request after adding the flash message. Thus the next request will be when you access another link.
It is only at the end of this next request, then after the gsp rendering, that the flash message will be cleared from session by grails framework.
To conclude :
if you want to display a message when you do a redirect use flash message. It's automatically cleared by the framework.
if you want to display a message when you do a forward you can use a solution as stated by fonger (add a message in the request).

Resources