refactor "render format" in multiple controllers - ruby-on-rails

so i've got a view method in multiple controllers which mostly looks exactly the same:
def show
show! do |format|
format.json do
if #text.activated?
#text.log
render_for_api :texts_all, :json => #text
else
render :nothing => true
end
end
format.pdf do
pdf = QrPdf.new(#text)
send_data pdf.render, filename: "text_#{#text.id}.pdf", type: "application/pdf"
end
end
end
the models for this are different, but they all have the same attributes that are used in this method (activated, log, id). i also could change the render_for_api given hash from which is currently texts_all, documents_all etc to a hash that its everywhere the same.
is there a way to use this code in multiple models without having this enormous duplication?
i'm thankful for every hint!
especially i find it hard to deal with the do |format| block. but also i'm not sure where to put the code and how to use it with different types of models.
thank you.

If the model is truly generic:
def show
show_model #text
end
I'm not sure what show! is, but that part you can figure out. Roughly (untested):
def show_model(obj)
show! do |f|
f.json do
return render(:nothing => true) unless obj.activated?
obj.log
render_for_api :texts_all, :json => obj
end
f.pdf do
opts = { filename: "text_#{obj.id}.pdf", type: "application/pdf" }
send_data QrPdf.new(obj).render, opts
end
end
end
As far as where show_model lives, I tend to put things like that into a base controller, or as a mixin, but there may be better options. Since I usually have a base controller, it's just easy to keep it there.

Related

What's the difference between using render instead of respond_with/to in a Rails API?

I am building a simple rails tutorial on how to build APIs for some students and I am building it without the respond_to and respond_with because I just want to see if I can build an api without using a gem. This is what I have and my tests pass:
controller:
class Api::V1::SuyasController < ApplicationController
def index
render json: Suya.all
end
def create
render json: Suya.create(suyas_params)
end
private
def suyas_params
params.require(:suya).permit(:meat, :spicy)
end
end
routes:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :vendors
resources :suyas
end
end
end
tests:
require 'test_helper'
class Api::V1::SuyasControllerTest < ActionController::TestCase
test "index can get all the suyas" do
Suya.create(meat: "beef", spicy: true)
Suya.create(meat: "kidney", spicy: false)
get :index
suyas = JSON.parse(response.body)
assert_equal "beef", suyas[0]["meat"]
assert_equal true, suyas[0]["spicy"]
assert_equal "kidney", suyas[1]["meat"]
assert_equal false, suyas[1]["spicy"]
end
test "create can create a suya" do
assert_difference("Suya.count", 1) do
create_params = { suya: { meat: "beefy", spicy: true }, format: :json }
post :create, create_params
suya = JSON.parse(response.body)
assert_equal "beefy", suya["meat"]
assert_equal true, suya["spicy"]
end
end
end
What's the difference between using render vs respond_with? I can't find any answers. Is there even something that I am doing wrong? Why are there two ways to create APIs (respond_to/respond_with AND this way?)
-Jeff
render is part of Rails and it just renders whatever you say in whatever format you say. Typically a view, possibly a string, possibly a file.
A pretty low-level function that renders whatever you say making a few assumptions per conventions, like where to look for a view.
respond_to is a micro-DSL that allows you to respond differently to different formats being requested.
I. e. in a block with |format| call to format.json requires a block that will be executed on requests for JSON, otherwise will be a no-op (no operation). Also, if respond_to didn't execute any block, it responds with a generic 406 Not Acceptable (server cannot respond in any format acceptable by the client).
While it is possible to do if request.json?, it's not so readable and needs to explicitly specify when to respond with 406.
respond_with, formerly part of Rails, now (since 4.2) in a separate gem responders (for a reason), takes an object and uses it to construct a response (making a lot of assumptions, all of them can be given at controller declaration).
It makes code much shorter in typical use cases (i. e. some APIs). In not-so-typical use cases it can be customized to suit your needs. In highly unusual use cases it's of no use.
I may be overly simplifying things, it's a general overview.
There are two things :)..render and respond_to.
Render is used to create a full response and sends it back to the browser.
So render is used in respond_to ,to make your action very responsive for every call whether it can be js/ajax call,full page load(html),json(to show autosearch dropdown,tokens) or xml.So if i want my method to work and respond to every calls from client,i will use the below block in my action.
respond_to do |format|
format.html { redirect_to(person_list_url) }
format.js {render "show_person_details"}
format.xml { render :xml => #people.to_xml }
format.json { render json: #people}
end
above controller will work on every scenario,such as js/html/json and xml without getting 403 Forbidden error which we get usually get when a js call is made to action having only format.html and not format.js
HOPE IT HELPS
I think the answer is that render only allows me to respond with JSON, whereas if I use respond_to and respond_with, I can respond in more than just one manner? Is that all?

as_json only working for some attributes

It appears that as_json is working for some of my attributes, but not all. Could someone tell me if anything here looks wrong? It's the "type" attribute that isn't working.
def as_json(options = {})
{
id: self.id,
type: self.type,
name: self.name
}
end
def index
#streams = #current_user.streams
respond_to do |format|
format.html { render :index }
format.json { render :json => #streams.as_json }
end
end
There are a couple issues with this implementation. Most importantly, #as_json is incorrectly overridden, which means nothing is running through the default ActiveModel::Serializers::JSON#as_json method. You probably want to read the detailed documentation located here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
Assuming all those values are simply attributes on the object, you would want your #as_json method to look like:
def as_json(opts = {})
super(opts.merge(only: [:id, :type, :name]))
end
If any of them are methods, rather than simply attributes, they would need to be included separately.
A minor point is that, when rendering an object to JSON using render json: #streams, you do not need to manually call #as_json. This is done automatically as part of the rendering and is not something you need to worry about.

CSV downloading the object and not actual values Rails

I'm not quite sure what I am overlooking when attempting to allow csv download on my Game model and I'm getting a little lost.
On the profile show page I render an index like list of games associated with that user, i.e. their game schedule.
The Profiles Controller-
def show
#user = User.find_by_profile_name(params[:id])
if #user
#listings = #user.listings
#games = #user.games
respond_to do |format|
format.html
format.csv {send_data #games.to_csv}
end
return
render action: :show
else
render file: "public/404", status: 404, formats: [:html]
end
end
Then in the game.rb I define the method to_csv
def self.to_csv
CSV.generate do |csv|
csv << column_names
all.each do |item|
csv << item.attributes.values_at(*column_name)
end
end
end
And on the profile show page to download the expected csv game schedule
<%= link_to "Download my Schedule", profile_path(format: 'csv')%>
I believe this might be my issue lies, but that doesn't quite explain what I get in my csv which is just a game object
file-
Here is my routes.rb
resources :games
match 'friendships/:friend_id' => 'user_friendships#new', :as => :new_friendship
match 'dashboard' => 'dashboard#show', :as => :dashboard
root to: "profiles#index"
get '/players', to: 'profiles#index', as:'players'
get '/players', to: 'profiles#index', as:'users'
get '/:id', to: "profiles#show", as: 'profile'
The file should be formatted with the column names (location, opponent, time, etc) as the header line and the corresponding lines with their respective values for each instance associated to a user.
I think the to_csv method inside game should be re-declared as -
passed in the games array which needed to be converted.
the param passed to values_at is column_names not column_name.
def self.to_csv(games)
CSV.generate do |csv|
csv << column_names
games.each do |item|
csv << item.attributes.values_at(*column_names)
end
end
end
and in the controller, the code should be:
def show
#user = User.find_by_profile_name(params[:id])
if #user
#listings = #user.listings
#games = #user.games
respond_to do |format|
format.html
format.csv {send_data Game.to_csv(#games)}
end
return
render action: :show
else
render file: "public/404", status: 404, formats: [:html]
end
end
otherwise, you will output all the games no matter which user you are using.
Although cenjongh's answer isn't wrong, let me elaborate on it.
The syntax Game.to_csv(#games) goes against Ruby's/Rails' object oriented approach for me.
Since the CSV generation code in your case is totally model independent (you don't make any assumptions of column names, etc.) you could feed this method any array of models, i.e. Game.to_csv(#shampoos) which would still work but wouldn't read very well.
Since Rails scopes the all method according to the criteria attached to the ActiveRelation object, using it in your class method wouldn't result in an output of all the games.
Assuming you're using at least Rails 3.0 the line #games = #user.games would give you an ActiveRelation object, not an array, meaning you can call #games.to_csv (or to make it even clearer #user.games.to_csv) directly, which reads what it is, namely converting a list of games, that belong to a user into CSV.
Oh, and I guess this is just testing code, but the return shouldn't be there. And the render statement should go into the block of format.html.

Rails ActiveRecord relations - avoiding writing .blank? checks

This is more a style question than anything.
When writing queries, I always find myself checking if the result of the query is blank, and it seems - I dunno, overly verbose or wrong in some way.
EX.
def some_action
#product = Product.where(:name => params[:name]).first
end
if there is no product with the name = params[:name], I get a nil value that breaks things.
I've taken to then writing something like this
def some_action
product = Product.where(:name -> params[:name])
#product = product if !product.blank?
end
Is there a more succinct way of handling nil and blank values? This becomes more of a headache when things rely on other relationships
EX.
def some_action
#order = Order.where(:id => params[:id]).first
# if order doesn't exist, I get a nil value, and I'll get an error in my app
if !#order.nil?
#products_on_sale = #order.products.where(:on_sale => true).all
end
end
Basically, is there something I haven;t yet learned that makes dealing with nil, blank and potentially view breaking instance variables more efficient?
Thanks
If its just style related, I'd look at Rails' Object#try method or perhaps consider something like andand.
Using your example, try:
def some_action
#order = Order.where(:id => params[:id]).first
#products_on_sale = #order.try(:where, {:onsale => true}).try(:all)
end
or using andand:
def some_action
#order = Order.where(:id => params[:id]).first
#products_on_sale = #order.andand.where(:onsale => true).andand.all
end
Well even if you go around "nil breaking things" in your controller, you'll still have that issue in your views. It is much easier to have one if statement in your controller and redirect view to "not found" page rather than having several ifs in your views.
Alternatively you could add this
protected
def rescue_not_found
render :template => 'application/not_found', :status => :not_found
end
to your application_controller. See more here: https://ariejan.net/2011/10/14/rails-3-customized-exception-handling

render layout stored in database

Doing some integration work with another site I've got the unusual requirement of needing to create the layout at runtime.
At the moment I'm having to resort to something like this:
def new
body = render_to_string 'new', :layout => false
page = add_layout(body, db.load_template)
render :text => page
end
This is a bit awkward, I'd rather do something like:
def new
...
render 'new', :layout => db.load_template
end
Is there a cleaner way to do this? Perhaps it's possible to register new layouts at runtime and use the normal syntax?
Ha! I encountered a project that will solve just that. Check out panoramic. It stores rails views in the database instead of the filesystem.
You can extend ActionController::Base (or ApplicationController) with a module and alias_method_chain to make this work.
module Foo
alias_method_chain :render, :dblayout
def render_with_dblayout options = nil, extra_options = {}, &block
if options.include? :dblayout
...
else
render_without_dblayout options, extra_options { yield }
end
end
end
ActionController::Base.send(:include, Foo)

Resources