I have an action triggered by an AJAX request generated by Ajax.InPlaceEditor or InPlaceCollectionEditor like this:
new Ajax.InPlaceCollectionEditor('agent_email', 'inspections/<%= #inspection.id %>/update_field',
{
collection: [<% #agents.each do |agent| %>
'<%= agent.email %>',
<% end %>],
okText: 'Update',
cancelText: 'Never mind',
savingText: 'Updating...'
});
At the other end, the action contains this:
def update_field
--some code here--
if success
puts "stored change"
render :text => result
else
puts "did note change store"
render :text => inspection.errors.to_json, :status => 500
end
end
Once any of the render methods are reached, the session expires, and next time the user send a request, Devise sends them to the logon on page.
Even though I am exempting update_field from authentication (before_filter :authenticate_user!, :except => :update_field), the session is still getting reset.
I have looked at the answer at a very similar question at Devise session immediately expiring on .js call [AJAX], but it is not solving my particular problem.
Any ideas?
I got this to work by getting the code from http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails (prototype-snippet.js):
/*
* Registers a callback which copies the csrf token into the
* X-CSRF-Token header with each ajax request. Necessary to
* work with rails applications which have fixed
* CVE-2011-0447
*/
Ajax.Responders.register({
onCreate: function(request) {
var csrf_meta_tag = $$('meta[name=csrf-token]')[0];
if (csrf_meta_tag) {
var header = 'X-CSRF-Token',
token = csrf_meta_tag.readAttribute('content');
if (!request.options.requestHeaders) {
request.options.requestHeaders = {};
}
request.options.requestHeaders[header] = token;
}
}
});
... within a Javascript block in my application.html.erb:
<script type="text/javascript">
(... the code from above)
</script>
Also don't forget to add:
<%= csrf_meta_tag %>
in the same file towards the top (if not already there).
The document "CSRF Protection Bypass in Ruby on Rails" explains why this works.
Related
I have inherited a Rails code base that I do not fully understand. We have a requirement to, when the user hits Submit, render ON THAT PAGE the set of validation failures the user put into the form. I cannot redirect to any other page - we must remain on the page which contains the form upon which they put the invalid input.
Here is my method
def tpr_bulk_update
updated, date_died = update_tprs #it returns 0,0 when there are validation fails
if updated == 0 && date_died == 0
flash.now[:notice] = 'A bunch of errors occurred'
#Here I need to refresh the page. I do not especially want to redirect.
#I want this to perform exactly the same as me hitting the refresh button on my browser.
#The initial form loaded via a very complicated codebase that I do not understand exactly.
#I do have available to me the params from the initial call - but it seems to me hitting refresh on
#the browser implicitly handles repassing to this method with the SAME PARAMS I CAME IN WITH....
#AND it shows my flash.now. So then, I want to refresh the page the same mechanism the browser uses,
#because this is what demonstrably meets my requirement
elsif !date_died
redirect_to tprs_index_vod_assets_path
else
flash[:notice] = "One or more TPR assets were not given valid future dates, so those invalid dates did not save".html_safe
redirect_to tprs_index_vod_assets_path
end
end
The issue is I see no way to do this. Perhaps the browser invoking refresh uses javascript that is impossible to inline in my rails controller?
redirect_to :back
fails on account that the set of params I came in with are not populated - it explodes.
respond_to do |format|
format.js {render inline: "location.reload();" }
end
My method does not output javascript, and neither will it ever output javascript - that is a requirement for the system.
I need whatever is equivalent to the refresh operation my browser (Chrome) performs when I press "Refresh". I want that to happen right after I set my flash.now message. How can I capture what Chrome/what hitting refresh actually does? And how can I perform it within my controller?
This is exactly what AJAX was designed for. By sending an asyncronous request you can send data to the server without reloading the page and update the page with the response. In Rails you can use Server Side Concerns to simply replace a chunk of contents with a rendered view.
Since you don't actually have an example of the form or controller this is a simplefied example that demonstrates the concept:
class ThingsController < ApplicationController
def new
#thing = Thing.new
end
def create
#thing = Thing.new(thing_params)
respond_to do |format|
if #thing.save
format.js
else
format.js { render :new }
end
end
end
end
end
# things/_form.html.erb
# remote: true is the default - its just added for extra clarity here
<%= form_with(model: #thing, remote: true, id: 'my-special-form') do |form| %>
<% if #thing.errors %>
# .. display the errors
<% end %>
# ...
<% end %>
# things/new.html.erb
<%= render partial: 'form' %>
// things/new.js.erb
// Since we want to extract the children of the form element
// we use DOMParser to create a fragment
const node = new DOMParser().parseFromString(
"<%= j render(partial: 'form') %>", // rails renders the template
"text/html"
);
// Find form on page and replace its contents
document.getElementById('my-special-form').innerHTML = node.querySelector('form').innerHTML;
// #todo set flash message
// things/create.js.erb
window.location = "<%= thing_path(#thing) %>";
The way that this works is that Rails UJS listens for submit events on any element with the data-remote attribute. Instead of the normal submit it will send an XHR request with the Content-Type: application/javascript header.
After rails is finished rendering and reponding with your js.erb view it is sent back to the client and Rails UJS takes the response and evals it by popping it into a script tag.
I rely on a variable defined by a JavaScript object to update (re-render) a Ruby on Rails partial inside a view, but it is not working. From all I read it tells me the only possible way for this to work is to use an Ajax call, however I'm new to this and couldn't quite grasp why (given the JavaScript variable is available before the Rails command is defined), nor exactly how I should do it.
My Rails view has this HTML bit I'm looking to
<div id="myevent">
<% if #pocket.events.any? %>
<%= #event = #pocket.events.first %>
<%= render #event %>
<% end %>
</div>
On that same view I implement a JavaScript object made up of various clickable nodes, each node denoting an event with a unique id. I want to re-render the #myevent section above each time someone clicks on a different node.
As someone new to front-end programming, I've tried this:
timeline.on('click', function (properties) {
logEvent('click', properties);
var item = properties["item"];
$('#myevent').html("<%= escape_javascript render (#pocket.events.find_by id:" + item + ") %>");
The JavaScript variable 'item' contains the event id from the clicked node. As you may know, the last line above doesn't work, Rails raises an ArgumentError with the message:
'nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.
It does works if I set a hardcoded id, so the rationale seems to be right:
$('#myevent').html("<%= escape_javascript render (#pocket.events.find_by id: 5) %>");
I have tried using the partial format as well:
$('#event').html("<%= escape_javascript render :partial=>'shared/event', :locals=> {event_id: " + item + "} %>")
And then using the following method to recover the right event in the 'shared/event' partial:
<%= #event = Event.find(event_id) %>
however, it fails with a ActiveRecord::RecordNotFound error because it doesn't really replace the string "+ item +" by the value of that JavaScript variable:
Couldn't find Event with 'id'=+item+
The "pain" is that, no matter how I try I can't find a way to use the JavaScript defined variable on that Rails calls. It looks like the solution would be to use an Ajax call but I reckon after many tries I don't get to work this out by myself. I would really appreciate a hand here.
I think that what happens is that the .erb portion of the code is only parsed at render time, so when the page first renders, there's really no item to append to that. In turn, when you hardcode it, it's able to parse that ruby code and render correctly.
So indeed you need to contact the server.
What I would do would be:
timeline.on('click', function (properties) {
var item = properties["item"];
$.ajax({
type: "POST",
url: '/route_to_your_controller',
data: { event_id: item },
dataType: 'json',
success: function(data) {
if (data['success']) {
$('#myevent').html(data['html']);
}
else {
alert('oopsss take care of this');
}
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log(errorThrown);
}
});
});
routes.rb
post '/route_to_your_controller', to: 'controller#spew_html'
then, saying you have a route to this action on a controller:
def spew_html
# you should take care of failed finds, errors that might occur, have a before_action to check legality of ajax call, and even possibly accept only xhr, return false unless request.xhr? etc
event_id = params[:event_id]
html_partial = render_to_string 'shared/event', locals: { event_id: event_id }
respond_to do |format|
format.json { render json: { html: html_partial, success: true } }
end
end
As a bonus, one pattern I like to implement when possible is just having the same route for GET be the POST for whatever ajax, so that then I can simply do:
$.ajax({
type: "POST",
url: window.location.pathname,
//...
});
And let the controller know how to deal with it.
I'm trying to implement javascript polling in my app but I'm running into a few problems. I'm pretty much following along with this railscasts. My problem is in trying to prepending any new data found. It prepends all of the data old and new and if there isn't any new data found it just prepends all of the old data. My other problem is that my setTimeout is only being called once, even after I try to keep it polling like they show in railscast. Below is my code. What am I doing wrong here?
polling.js
var InboxPoller;
InboxPoller = {
poll: function() {
return setTimeout(this.request, 5000);
},
request: function() {
return $.getScript($('.inbox_wrap').data('url'), {
after: function() {
$('.conversation').last().data('id')
}
});
}
};
$(function() {
if ($('.inbox_wrap').length > 0) {
return InboxPoller.poll();
}
});
polling.js.erb
$(".inbox_wrap").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
InboxPoller.poll();
conversations_controller.rb
class ConversationsController < ApplicationController
before_filter :authenticate_member!
helper_method :mailbox, :conversation
def index
#messages_count = current_member.mailbox.inbox({:read => false}).count
#conversations = current_member.mailbox.inbox.order('created_at desc').page(params[:page]).per_page(15)
end
def polling
#conversations = current_member.mailbox.inbox.where('conversation_id > ?', params[:after].to_i)
end
def show
#receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
render :action => :show
#receipts.mark_as_read
end
def create
recipient_emails = conversation_params(:recipients).split(',').take(14)
recipients = Member.where(user_name: recipient_emails).all
#conversation = current_member.send_message(recipients, *conversation_params(:body, :subject)).conversation
respond_to do |format|
format.html { redirect_to conversation_path(conversation) }
format.js
end
end
def reply
#receipts = conversation.receipts_for(current_member).order('created_at desc').page(params[:page]).per_page(20)
#receipt = current_member.reply_to_conversation(conversation, *message_params(:body, :subject))
respond_to do |format|
format.html { conversation_path(conversation) }
format.js
end
end
private
def mailbox
#mailbox ||= current_member.mailbox
end
def conversation
#conversation ||= mailbox.conversations.find(params[:id])
end
def conversation_params(*keys)
fetch_params(:conversation, *keys)
end
def message_params(*keys)
fetch_params(:message, *keys)
end
def fetch_params(key, *subkeys)
params[key].instance_eval do
case subkeys.size
when 0 then self
when 1 then self[subkeys.first]
else subkeys.map{|k| self[k] }
end
end
end
def check_current_subject_in_conversation
if !conversation.is_participant?(current_member)
redirect_to conversations_path
end
end
end
index.html.erb
<%= content_tag :div, class: "inbox_wrap", data: {url: polling_conversations_url} do %>
<%= render partial: "conversations/conversation", :collection => #conversations, :as => :conversation %>
<% end %>
_conversation.html.erb
<div id="conv_<%= conversation.id %>_<%= current_member.id %>" class="conversation" data-id="<%= conversation.id %>">
<div class="conv_body">
<%= conversation.last_message.body %>
</div>
<div class="conv_time">
<%= conversation.updated_at.localtime.strftime("%a, %m/%e %I:%M%P") %>
</div>
</div>
Javascript polling is extremely inefficient - basically sending requests every few seconds to your server to listen for "updates". Even then, in many cases, the updates will be entire files / batches of data with no succinctness
If we ever have to do something like this, we always look at using one of the more efficient technologies, specifically SSE's or Websockets
--
SSE's
Have you considered using Server Sent Events?
These are an HTML5 technology which work very similarly to the Javascript polling - sending requests every few seconds. The difference is the underlying way these work -- to listen to its own "channel" (mime type text/event-stream -- allowing you to be really specific with the data you send)
You can call it like this:
#app/assets/javascript/application.js
var source = new EventSource("your/controller/endpoint");
source.onmessage = function(event) {
console.log(event.data);
};
This will allow you to create an endpoint for the "event listener":
#config/routes.rb
resources :controller do
collection do
get :event_updates #-> domain.com/controller/event_updates
end
end
You can send the updates using the ActionController::Live::SSE class:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
include ActionController::Live
def event_updates
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
sse.write({ name: 'John'})
sse.write({ name: 'John'}, id: 10)
sse.write({ name: 'John'}, id: 10, event: "other-event")
sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
ensure
sse.close
end
end
--
Websockets
The preferred way to do this is to use websockets
Websockets are much more efficient than SSE's or standard JS polling, as they keep a connection open perpetually. This means you can send / receive any of the updates you require without having to send constant updates to the server
The problem with Websockets is the setup process - it's very difficult to run a WebSocket connection on your own app server, hence why many people don't do it.
If you're interested in Websockets, you may wish to look into using Pusher - a third party websocket provider who have Ruby integration. We use this a lot - it's a very effective way to provide "real time" updates in your application, and no I'm not affiliated with them
Are you sure that polling is happening only once? Did you check in the console to make sure of that?
To me it looks like it might be happening, because I see issues in your javascript that prepends the content. What you have is this:
$(".inbox_wrap").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
This will actually insert the new content before inbox_wrap div. I think your intention is to prepend to the conversations list. For that, you should change it to this (Reference jQuery Docs: http://api.jquery.com/prepend/):
$(".conversation").prepend("<%= escape_javascript(render #conversations, :locals => {:conversation => #conversation}) %>");
The next thing is that I am assuming you want to prepend the new conversations on top of the list, which means two things.
The controller will return new conversations ordered by date in descending order.
Assuming above is correct, you would need to get the first conversation's id in your InboxPoller to pass to the controller as after parameter.
$('.conversation').first().data('id')
One More Thing
Another thing is that you can use the native Javascript function setInterval instead of setTimeout. setInterval executes a given function periodically, as opposed to setTimeout which does it only once. This way you won't have to initiate your InboxPoller after every call to the back-end, in .js.erb file.
Update
Again, looking at jQuery Documentation, it looks like $.getScript() does not pass any parameters back to the server. Instead use $.get as below:
InboxPoller = {
poll: function() {
return setInterval(this.request, 5000);
},
request: function() {
$.get($('.inbox_wrap').data('url'), {
after: function() {
return $('.conversation').first().data('id');
}
});
}
};
Also, I think you need to add .order('created_at desc') in polling method in your controller.
I saw this post about this issue:
Auto Refresh a Html table every x seconds
Now, i am using rails, and i would like to do the same thing, but i want to tell rails that i want a remote page, i dont want it to load the full page.
All i want is pretty much to simulate :remote => true action with ajax.
I've tried it with regular ajax, and rails loads the whole page instead of just the relevant content that i want.
How can i do it ?
Your code should be something like this:
Your controller
def your_action
# your code goes here
end
(your_action.js.erb)
$("#div_id").html("<%= escape_javascript reder :partial => "your_partial_for_table" %>")
your_action.html.erb
<div id="div_id">
<%= render :partial => "your_partial_for_table" %>
</div>
_your_partial_for_table.html.erb
#your table goes here
<script>
$(function() {
$(document).ready(function() {
setInterval(function() {
jQuery.ajax({
url: "<path to your_action>",
type: "GET",
dataType: "script"
});
}, 30000); // In every 30 seconds
});
});
</script>
Server Sent Events
You may wish to use Server Sent Events (from HTML5):
Server-sent events (SSE) is a technology for where a browser gets
automatic updates from a server via HTTP connection. The Server-Sent
Events EventSource API is standardized as part of HTML51 by the W3C.
This basically initiates something called ajax long-polling, which is where your JS will "ping" a particular URL every second, or number of seconds.
The Ajax polling requests will then bring back particular response from the server, allowing you to use as you wish
--
Layout
I would do this:
#app/assets/javascripts/application.js
var source = new EventSource('path/to/your/endpoint');
source.addEventListener('message', function(e) {
//do something here
}, false);
#app/controllers/your_controller.rb
Class YourController < ApplicationController
include ActionController::Live
def your_action
response.headers['Content-Type'] = 'text/event-stream'
sse = Reloader::SSE.new(response.stream)
begin
sse.write(last_updated, event: 'results')
rescue IOError
# When the client disconnects, we'll get an IOError on write
ensure
sse.close
end
end
end
The trick here is that SSE's use their own content type - text/event-stream to receive & parse the required data. This is compared with the standard mime types which governs the typical HTTP protocol
I used this url for reference - you can then manipulate the text as it comes back from the server!
/* with jQuery */
jQuery(documenta).ready(function() {
setTimeout(function() {
jQuery('.table').load(...);
, 5000);
});
# with rails, determine if it is a ajax request
if request.xhr?
# respond to Ajax request
else
# respond to normal request
end
see also here Rails detect if request was AJAX
Check this out. I've got a fairly simple form that's created with the following syntax:
<%= form_for([#issue, #issue_order], :remote => true) do |f| %>
The form, due to logic on the page, is actually called via javascript, like this:
$('#new_issue_order')[0].submit()
The controller handles the ajax request by doing a bit of logic then throwing out a little something like this:
respond_to do |format|
format.js
end
The AJAX that handles this response is in the following javascript:
$('#new_issue_order').on('ajax:success', issueOrder.processOrder)
..........
processOrder: function(e, data, status, xhr) {
$('.sign-up-errors').empty();
errors = xhr.getResponseHeader('X-Flash-Error').split(',');
for (i=0; i < errors.length; i++) {
$('.errors').append($('<p>' + errors[i] + '</p>'));
}
setTimeout(function() {
$('.errors').empty();
}, 3500);ยท
}
I figured that would allow it to respond to the remote request, but what I get instead is the following error:
ActionController::UnknownFormat
I tried creating a new.js.erb in my views (to correspond with the new page that it was on), but I'm still getting the same error. I haven't tried migrating my success handler AJAX to the new.js.erb code, because I'd prefer to keep my javascript handling in the javascript file in my assets for business reasons.
How can I get a seamless AJAX response? I've done it before, but respond_to has always confused me.
Setting the js response template as new.js.erb is incorrect.
The form itself is within new.html.erb template, guessed by convention. So, the form's action is supposed to point to #create.
In order to response correctly to this form's submission, you need to create a js template as create.js.erb, and respond to js in #create action.
Besides, in most cases you don't need to manually set Ajax response in assets js like
$('#new_issue_order').on('ajax:success', issueOrder.processOrder)...
Instead, you can just do it within create.js.erb. For example
$('#new_issue_order').css('background', 'green')
This script will be run after ajax:success event.