Rails 3 Layout + Querystring Problem - ruby-on-rails

I'm trying to setup an ajax-based website that loads a fragment of a webpage when a specific combination of GET variables and HTTP Headers are given.
GET /normal/html/page?ajax=true
X-ajax: true
What I've setup in my controller is:
before_filter do |controller|
if request_by_ajax?
ApplicationController.layout false
end
end
This works, but only in development mode. You see when I attempt to access the given page it only gives the fragmented (layout free) page.
When the normal page is accessed without ajax:
GET /normal/html/page
This returns only the view for that page and NOT the layout around it. But if I access that page when the webserver is reloaded then it returns the layout and when its accessed afterwards via AJAX it returns the layout + the view. Clearly there is a caching issue here.
I would really like to keep the same route for the page. If there is an ajax call then I would prefer to figure out the response based on the querystring and request header values. But rails prefers to classify querystring parameters and request headers as meaningless when serving a file (i.e. /normal/html/page and /normal/html/page?ajax=true) return the same actual template file (this what I assume).
Any idea how to get around this?

I figured out the issue.
It turns out that the layout call must be defined early on in the application controller:
---- application_controller.rb
layout :choose_layout
def choose_layout
if request_by_ajax?
false
else
'application'
end
end
Works like a charm.

Related

Ruby on Rails Write a redirect for a dynamic URL to a static URL

Is there a simple way to write a ruby if statement to redirect a dynamic page to a static page?
I’m writing this in my pages controller where the site pages are generated. This is what I’m attempting to do.
If “/index/page1”
redirect_to “page2”
return
end
This redirects all the pages created in the pages controller to “page2”. I know the syntax is incorrect. I need help in writing out the correct way to test for the first condition.
Any help is appreciated. Thank you!
Here is an update / more information to my question.
Here is my show action in the PagesController
def show
#page = Page.find_by_url_path("/#{params[:url_path]}")
layout = "templates/#{#page.pageable.class.name.underscore}"
respond_to do |format|
format.html { render layout: layout }
end
I need to write an if statement that looks for one specific URL that gets generated. This page is created by the show action.
e.g. https://host.com/products/page1
Then redirect it to another specific URL. This is a static page on the site.
e.g. https://host.com/page2
I am having difficulty in writing the if statement to find the first page. This is what I've tried.
if "/products/page1"
redirt_to "/page2"
return
end
Depending where I put the code within the show action, I either get a double render error (as subparry explains below). Or I redirect all the pages generated through the show action to "/page2".
This application was written by a more experienced Ruby developer and I'm doing my best to maintain / update it. If I need to post more detailed information, please let me know. Thank you.
Well, as usual with these kind of requirements, there are many ways to achieve it.
As you know, different pages (views) in controllers are represented by instance methods (or Actions), for example I can imagine your PagesController looking something like this:
class PagesController < ApplicationController
def page1
# do something...
end
def page2
#do something else...
end
end
So, the easiest way would be to trigger a redirect from page1 redirect_to "https://host.com/page2", I don't know the reasons behind the decision to redirect. If it is a temporary redirect, it might be the best solution because of ease of change later, but if it is a more permanent redirection, I would implement it at the web server level (Nginx for example)
It depends on your use case.
PS: Don't forget that if you redirect in your action, it does not imply a return, so if you have more code below and another call to render or redirect, it will fail (double render error) so either you remove further renders/redirects or insert an early return.
EDIT:
Ok, now I understand better your case. You have a model called Pages which has a column called url_path which details the location of each page.
So, if I understand correctly, you'll have to do the conditional statement like this:
def show
if params[:url_path] == 'page1'
redirect_to 'https://host.com/page2'
return
end
# Rest of action code...
end
I don't know for sure how are paths stored in url_path, but you get the idea!
PS2: When you write if 'products/page1' you are basically saying if true and always entering the condition because only nil and false are falsy values, everything else is truthy.

Rails 3 Specify Fallback Format if MissingTemplate

I have a rails 3.2 application that recently I added mobile-fu gem to, in order to add separate mobile views.
There were a few hiccups but, for the most part, it works wonderfully.
However, I've only made mobile views for a handful of pages. When I attempt to go to a page that does not have a mobile view, from a mobile device, I get:
Missing template after_hour_it_supports/index, application/index with {:locale=>[:en], :formats=>[:mobile], :handlers=>[:erb, :builder, :prawn, :prawn_dsl]}
Which is what I would expect.
However, it is necessary to display some pages which might never have mobile counterparts. Specifically, there is a page to approve something. The thing that they are approving might never have a mobile view made for it, but the approval page already has a mobile view. My current approach, is to use an iframe to display the possibly non-mobile view. Of course, if a mobile view exists, I would prefer to use it over the non-mobile view (still in the iframe).
So what I would like to do is to attempt to render template with the :mobile format, but if the mobile format does not exist, to render with the :html format, which seems like something rails already does based on the :formats array mentioned in the MissingTemplate exception. I can find some documentation on how to set the :formats array when calling render, but I would like to do this automatically, without having to modify every existing response.
How do I modify :formats=>[:mobile] to be :formats=>[:mobile, :html] on an application level?
I finally found an answer which seems to work for me (the answer by Will Madden).
Specifically, I used his suggestion to override the formats= in my ApplicationController. Which he says is the same way that rails already adds this exact same functionality for the :js format.
The specific method he writes in his answer looks like this:
class ApplicationController
...
def formats=(values)
values << :html if values == [:mobile]
super(values)
end
...
end
This is by far the most elegant solution I have found for my circumstances. However, it fails whenever the controller for the page in question contains a respond_to block for the current action.

How to profile a rails controller that returns a json response with rack-mini-profiler?

I am using rack-mini-profiler in my rails 3.2 project.
In gemfile:
gem 'rack-mini-profiler'
Everything works great. But my application is mostly a set of json endpoints. So while it is very useful to be to able to inspect the performance of html pages, I would like to also be able to see the performance of controllers that return json.
Example of my controller:
class UsersController < BaseController
def json_method
# you don't see the mini profiler ui for this controller
render json: { users: [:foo, :bar]}
end
end
If I go to localhost:3000/users/json_method, I see my json response but not the profiler ui.
In development, by default, the rack-mini-profiler gem collects the previous JSON call and presents it in the menu accessible from the HTML page. No code change required.
So, make your JSON request, then hit any other HTML page and it will be available in the list. We use this to great effect. If your Rails app is a JSON API service only, make sure you have a 404.html in your public folder at least, then hit something like:
http://localhost/404.html
In the second tab, you can visit this URL /rack-mini-profiler/requests Where you can see the last request log
if rack mini profiler can’t display the results, it will collect them until it can on the next HTML page. So, the solution that I am using is to:
make the JSON request and
then hit an HTML page of my choice.
The results will appear, along with the most recent HTML profile.
http://tech.eshaiju.in/blog/2016/02/25/profile-api-endpoints-using-rack-mini-profiler/
As a first solution, you can just set the format to html, and render inside the html page:
The controller:
class UsersController < BaseController
def json_method
#users_json { users: [:foo, :bar]}
render 'index', formats: ['html']
end
end
And in app/views/users/index.html.erb:
Users:<br/>
<%= #json.inspect %>
I don't care so much about my json result, now I have the profiling ui.
A solution where I have the profiling ui without changing my controller would be much better.
Note that in a new Rails API project initialized using rails new api_name --api, the ApplicationController inherits from ActionController::API, instead of ActionAcontroller::Base. In this case, mini-profiler might not load when your HTML page is shown.
I had to change the base class to ActionController::Base to make it work. If in your app you see no requests to load resources from mini-profiler on your HTML page, you may want to try this change. Took me a long while to figure out.
Also note that you do need to have at least the <body> tag in your template to be rendered, otherwise the mini-profiler divs will not be properly injected.

#! causing routing issues using Angular/Rails with multiple layouts

I'm developing an AngularJS application with a RoR backend and ran into an issue when using multiple layouts. The application uses one layout when rendering pages to an unauthenticated user, and changes to another layout once the user is authenticated.
The layout is server on the initial pageload, and is managed by Rails. Some sample code illustrating how we're loading the different layouts based on the route:
class SampleController < ApplicationController
layout :current_layout
def current_layout
"layout" unless request.xhr?
end
end
Sample controller for different section:
class SampleController2 < ApplicationController
layout :current_layout
def current_layout
"anotherLayout" unless request.xhr?
end
end
This is defined separately for the controllers managing the authenticated/unauthenticated users, and basically serves up the proper layout. We're using a XHR check to prevent routing loops when Angular comes into picture.
So this works fine in most browser, but breaks when using IE9. Angular falls back to using #! URLs in IE9, so Rails has no idea which controller to load since the hash doesn't get sent to the backend. In this case, Rails loads the root and it's associated layout. If the authenticated section is set as the default, then it loads this layout even for unauthenticated users, and vice-versa.
So basically, I need to find a way to make this multiple layout application work properly even in browsers which don't support HTML5 pushState. I've checked all over the place for a proper solution for this and couldn't come up with anything yet.
Alright, so after testing lots of tweaks on both the client and server side, I ended up using the following solution. Basically, I just stopped exposing Angular to multiple layouts and dealt with that in Rails. Of course, this does put a limitation on users.
The app is divided into two main sections: dashboard and authentication. So in the backend, we restrict the users from accessing any authentication pages if they're logged in, and obviously dashboard pages can only be accessed if the user is authenticated.
The root of the problem was that Rails had no way of knowing which layout to serve, if a layout should be served at all. We used the cookies to differentiate the authenticated and unauthenticated users. If the cookie is set, and it passes the authenticity testing, then the dashboard layout should be loaded. Otherwise, any user trying to access the dashboard will be redirected to the authentication section:
class DashboardController < ApplicationController
layout :current_layout
before_filter :authenticate_user
def authenticate_user
if request.xhr? && !cookies[:access_token]
redirect_to "/login"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
Similarly for the authentication section:
class AuthenticationController < ApplicationController
layout :current_layout
before_filter :redirect_if_authenticated
def redirect_if_authenticated
if request.xhr? && cookies[:access_token]
redirect_to "/dashboard"
end
end
def current_layout
if cookies[:access_token]
"dashboard" unless request.xhr?
else
"application" unless request.xhr?
end
end
end
So main points to note here:
If the request is an XHR, don't serve the layout again. Serving the layout again will cause infinite loading loops in IE9, possibly in other IE versions as well.
Since we're not getting the URL Fragment at the server side, we have no way of knowing which layout should be loaded. So we're using the cookies as the source of truth, and access is controlled solely based on this.
This introduces some asymmetry on the clientside: In some cases, the url and layout will remain if the user types in the URL for another section. Since we can't do anything based on the URL from the server, and since cookies remain unchanged in this case, this has to be handled by Angular.
For the last point, the authentication section in my case only had a few possible urls, so I just redirected if I got a regex match as follows:
if(Modernizr.history) {
if(!/(login|sign|pass)/.test(location.pathname)) {
$location.path('/');
}
} else {
if(location.hash.length > 3 && !/(login|sign|pass)/.test(location.hash)) {
$window.location.href = '/';
}
}
Checking for the history API here, because in older IE browsers(and I guess in other older ones as well?), $location.path('/') wasn't reloading the entire page consistently. When using this, the $routeProvider configuration should also conditionally load the layouts, for cases where we redirect to root /:
$routeProvider
// Code for routes
.otherwise({
// Of course, add something to check access_token authenticity here as well :P
redirectTo: $.cookie('access_token') ? "/dashboard" : "/login"
});
So, with these couple tweaks, the app is working properly in all browsers and functioning with multiple layouts. Of course, I know nothing about RoR, so I'm hoping someone is going to have a few improvements to suggest, or a better answer. ;)

Custom error page in Rails for specific actions

I have a controller MyController that some of his actions cannot return full HTML pages but only the content without HTML/HEAD/BODY ...tags.
The default error pages are 500.html etc. are indeed full HTML pages and for these MyController:actions I need to somehow get the error pages in a non-full page format.
One way of doing it is:
Override the "render_optional_error_file()" inside the MyController and redirect in case the action is one my special actions to a different version of the error pages (content only, non-full page tags).
Will this work? any other way out there?
BTW: I am working with rails 2.3.9.
Thanks,
Erez
It's a bit of a difficult problem, particularly in the case of the 500 error. Since the 500 will render when the application encounters and error, you can't/shouldn't have a dynamic page that is handled by a controller - since this controller could be the source of the problem.
However, in the case of the 404, you could setup an ErrorsController with a 404 action, and use a different layout, depending on whether or not the request was an XHR request or not.
Then all you need is a method in your ApplicationController like:
def render_404
render "errors/404", :status => 404
end
Which will allow you to call your custom error handler wherever you like.
You could extend this further to add support for the 500 error too (where appropriate) but keep in mind that you will always need the static 500.html in your public folder, incase of a larger problem.

Resources