Does ActiveModel::Serializer require an explicit render call? - ruby-on-rails-4

I know that when using view templates (html, rabl), I don't need an explicit render call in my controller action because by default, Rails renders the template with the name corresponding to the controller action name. I like this concept (not caring about rendering in my controller code) and therefore wonder whether this is possible as well when using ActiveModel::Serializers?
Example, this is code from a generated controller (Rails 4.1.0):
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
#other actions
# GET /products/1
# GET /products/1.json
def show
end
end
and this is the serializer:
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :description, :url, :quantity, :price
end
Hitting /products/1.json, I would expect two things to happen:
Fields not listed in the serializer to be ommited,
Whole JSON object to be incapsulated within a 'product' top level field.
However, this does not happen, whole serializer is ignored. But then if I modify the Show method to the following:
# GET /products/1
# GET /products/1.json
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html
format.json { render json: #product }
end
end
And now it is all fine, but I have lost the benefit of the before_action filter (and it seems to me that I have some redundant code).
How should this really be done?

Without an explicit render or respond_with or respond_to Rails will look for a matching template. If that template does not exist Rails throws an error.
However, you can create your own resolver to bypass this. For instance, suppose you created app\models\serialize_resolver.rb and put this into it:
class SerializeResolver < ActionView::Resolver
protected
def find_templates(name, prefix, partial, details)
if details[:formats].to_a.include?(:json) && prefix !~ /layout/
instance = prefix.to_s.singularize
source = "<%= ##{instance}.active_model_serializer.new(##{instance}).to_json.html_safe %>"
identifier = "SerializeResolver - #{prefix} - #{name}"
handler = ActionView::Template.registered_template_handler(:erb)
details = {
format: Mime[:json],
updated_at: Date.today,
virtual_path: "/#{normalize_path(name, prefix)}"
}
[ActionView::Template.new(source, identifier, handler, details)]
else
[]
end
end
def normalize_path(name, prefix)
prefix.present? ? "#{prefix}/#{name}" : name
end
end
And then, in either your application controller (or in an individual controller) place:
append_view_path ::SerializeResolver.new
With that you should be able to do what you want. If it is a json request, it will create an erb template with the right content and return it.
Limitations:
This is a bit clunky because it relies on erb, which is not needed. If I have time I will create a simple template handler. Then we can invoke that without erb.
This does wipe out the default json response.
It relies on the controller name to find the instance variable (/posts is converted to #post.)
I've only tested this a little. The logic could probably be smarter.
Notes:
If a template is present, it will be used first. That allows you to override this behavior.
You can't simply create a new renderer and register it, because the default process doesn't hit it. If the template is not found, you get an error. If the file is found, it goes straight to invoking the template handler.

The 'redundant code' we see in the second one, is this line only:
#product = Product.find(params[:id])
And I believe this is the same logic as your before_action. You don't need this line, just remove it. Now the duplication is removed.
To the remaining part. An action needs to know what to render. By default if the action is empty or absent, the corresponding 'action_name'.html.erb (and other formats specified by respond_to) will be looked up and rendered.
This is why what the Rails 4 generator created works: it creates the show.html.erb and show.json.jbuilder which get rendered.
With ActiveModel::Serializer, you don't have a template. If you leave the action empty, it doesn't have a clue what to render. Thus you need to tell it to render the #product as json, by either:
render json: #product
or
respond_with #product

Related

Assign nested attributes records to current user when using Cocoon gem

In my application I have models Post & Slides & I have:
class Post < ActiveRecord::Base
has_many :slides, inverse_of: :post
accepts_nested_attributes_for :slides, reject_if: :all_blank, allow_destroy: true
Everything works fine, only thing I need (because of how my application will work), is when a slide is created, I need to assign it to current_user or user that is creating the record.
I already have user_id in my slides table and:
class User < ActiveRecord::Base
has_many :posts
has_many :slide
end
class Slide < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
My PostsController looks like this:
def new
#post = current_user.posts.build
// This is for adding a slide without user needing to click on link_to_add_association when they enter new page/action
#post.slides.build
end
def create
#post = current_user.posts.build(post_params)
respond_to do |format|
if #post.save
format.html { redirect_to #post, notice: 'Was successfully created.' }
else
format.html { render :new }
end
end
end
Any help is appreciated!
There are two ways to accomplish this:
First option: when saving the slide, fill in the user-id, but this will get pretty messy quickly. You either do it in the model in a before_save, but how do you know the current-user-id? Or do it in the controller and change the user-id if not set before saving/after saving.
There is, however, an easier option :) Using the :wrap_object option of the link_to_add_association (see doc) you can prefill the user_id in the form! So something like:
= link_to_add_association ('add slide', #form_obj, :slides,
wrap_object: Proc.new {|slide| slide.user_id = current_user.id; slide })
To be completely correct, you would also have to change your new method as follows
#post.slides.build(user_id: current_user.id)
Then of course, we have to add the user_id to the form, as a hidden field, so it is sent back to the controller, and do not forget to fix your strong parameters clause to allow setting the user_id as well :)
When I'm looking at this I see three ways to go about it, but since you're on cocoon already, I would drop the connection between user & slides - as it kind of violates good database practices (until you hit a point where you page is so popular you have to optimize of course, but that would be done differently).
You are using cocoon, but you're not utilizing the nesting of the relationship fully yet ...
The best practice would be to have cocoon's nesting create both & instead of trying to assign to current_user you call something like:
#slides = current_user.posts.find_first(param[:id]).slides
The #slides saves all the results, the .Post.find(param[:id]) finds a specific post for current_user.
Note: this is not the most optimized way & I haven't tested this, but it shows you the format of one way you can think about the relationships. You will need to hit rails console and run some tests like ...
(rails console)> #user = User.first
Next we test that there are posts available, as it's frustrating to test blanks & not get the results ...
(rails console)> #posts = #user.posts
Then we use the find method & I'm going to use Post.first just to get a working id, you can easily put "1" or any number you know is valid ...
(rails console)> #post = #posts.find(Post.first)
Finally, we go with either all slides to make sure its a valid dataset
(rails console)> #post.slides
If you want a specific slide later & have a has_many relationship just tag that find method on the .slides after.
Also one last thing - when you state earlier in there you need the current_user to be related, you can use an entry in your model.rb to create a method or a scope to get the data & allow you to link it to the current_user more easily & even drop some directed SQL query with the .where method to pull that information up if performance is an issue.
I spotted a second optimization in there ... if everything really is working - don't worry about this!
And don't forget about the strong_parameters nesting to do this fully ... Strong Param white listing
Basic format ... `.permit(:id, :something, slide_attributes: [:id, :name, :whatever, :_destroy])

How to implement DelayedJob custom job for Prawn PDF rendering?

Im trying to use DelayedJob to render Prawn PDFs. Following the custom job code in the docs, I've come up with this:
/lib/jobs/pdf_handling.rb
RenderPdf = Struct.new( :id, :view_context ) do
def perform
user = User(id)
pdf = UserFolder.new( id, view_context )
name = "user_folder_report.pdf"
send_data pdf.render, filename: name, type: "application/pdf"
end
end
PagesController.rb
def user_folder
respond_to do |format|
format.pdf do
Delayed::Job.enqueue RenderPdf.new(#user, view_context)
end
end
end
this results in the error:
uninitialized constant PagesController::RenderPdf
Adding required RenderPdf at the top of the PagesController doesn't help.
What am I missing? How can I implement this so PDF generation occurs via DelayedJob? Thanks.
updates
When /jobs is moved under /apps the error changes to:
can't dump anonymous module: #<Module:0x007fca7a3ae638>
/application.rb
config.autoload_paths += Dir["#{config.root}/lib/assets/"]
updates
I changed
class RenderFolder < Struct.new( :type, :rating_id, :dis, :view_context )
def perform
to
class RenderFolder < ActiveJob::Base
def perform(...)
Then, using ActiveJob, you can do
RenderFolder.perform_later(...)
This seems to be working...Im still implementing.
the lib folder is no longer loaded by default in rails. you can either add it to the autoload_path or (what i would do) just have it in some app/xxx folder. typically, i have app/support or something for arbitrary utility classes.

around_action for render view in controller rails

I have the following method in my controller
around_action :wrap_in_transaction_and_begin, only: :update
def wrap_in_transaction_and_begin
ActiveRecord::Base.transaction do
begin
render json: yield
rescue => e
ActiveRecord::Rollback
render json: { error: e.to_s }
end
end
end
but the problem is raising an error:
Missing template topics/update...
because rails expected "render" in method update itself, not in action_around.
But I need use render json with the returned value of my method update.
exemple:
def update
hash = { foo: :bar }
topic.update(hash)
hash
end
I want render in json the hash object.
how can I do that?
thx.
There's a bunch of things wrong here, mostly because you're diverging from how Rails is supposed to work into really unsupported work-flows.
First, the return value from an action isn't useful. Rails doesn't expect your action to return anything, and it doesn't do anything with the return value. Rails expects output from your action to be either a render or a redirect, and if you don't do either of those things, you're implicitly asking Rails to do the rendering for you. This is why Rails is attempting to render topics/update.
You're also misusing around filters. Their purpose is to transform the output of your action, not to by the sole generator of output.
The way you should be rendering JSON is using respond_with from your action. First, use respond_to to define the kinds of data that your controller can output...
class MyController < ApplicationController
respond_to :json
end
Then, respond with the value you want to respond with.
def update
hash = { foo: :bar }
topic.update(hash)
respond_with hash
end

Mongoid_slug Rails 4 update / destroy object

I am using mongoig_slug gem in my rails app.
I can create an object with the proper url but I can't update / delete the object and I have no error message in the console. (I can edit the form, which contains the correct data)
I even used PRY to check in the console and when I check #book exits and is correct, if I do #book.destroy it says true but does not destroy.
For the edit, I also checked #book, I also checked book_params which is correct.
class Book
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Slug
field :_id, type: String, slug_id_strategy: lambda {|id| id.start_with?('....')}
field :name, type: String
slug :name
end
class BooksController < ApplicationController
before_filter :get_book, only: [:show, :edit, :update, :destroy]
def update
if #book.update_attributes(book_params)
redirect_to book_path(#book)
else
flash.now[:error] = "The profile was not saved, please try again."
render :edit
end
end
def destroy
binding.pry
#book.destroy
redirect_to :back
end
def book_params
params.require(:book).permit(:name)
end
def get_book
#book = Book.find params[:id]
end
end
You can't just copy that line slug_id_strategy: lambda {|id| id.start_with?('....')} without changes. You should replace dots with something that defines is it id or not.
From docs:
This option should return something that responds to call (a callable) and takes one string argument, e.g. a lambda. This callable must return true if the string looks like one of your ids.
So, it could be, for example:
slug_id_strategy: lambda { |id| id.start_with?('5000') } # ids quite long and start from the same digits for mongo.
or:
slug_id_strategy: lambda { |id| =~ /^[A-z\d]+$/ }
or probably:
slug_id_strategy: -> (id) { id =~ /^[[:alnum:]]+$/ }
Updated
The latest version of mongoid_slug is outdated, you should use github version. So in your Gemfile:
gem 'mongoid_slug', github: 'digitalplaywright/mongoid-slug'
Also change field: _id line to:
field :_id, type: BSON::ObjectId, slug_id_strategy: lambda { |id| id =~ /^[[:alnum:]]+$/ }
Cause _id type is not a string, and this occurs error. This should work.

inherited_resources alternate create method forbiddenattributes

I have two create methods (maybe could be structured better, but new to inherited_resources)
Basically, I want to redirect to a different page after create, I am getting a ForbiddenAttributes error using one method, but not the original Create action, I'm guessing there is some special way to use IH, but I am stumped on this one.
In my second action, I need to manually assign the params - I'm guessing I need to do this the IH way, that line is where it blows up so the question is how is IH achieving this without an error?
def create
if can? :create, LeaveRequest
create! { leave_requests_url }
end
end
def manage_create
#leave_request = LeaveRequest.new(params[:leave_request])
if can? :create, LeaveRequest
create! { manage_leave_requests_url }
end
end
def permitted_params
{:leave_request => params.fetch(:leave_request, {}).permit(:user_id, :controller, :manager_id, :part_day, :comment, :selected_dates, :status, :leave_type_id, leave_dates_attributes:
[:id, :leave_request_id, :hours, :date_requested, :_destroy])}
end
Route is defined as
match 'manage_create', to: 'leave_requests#manage_create', as: :manage_create_leave_request, via: [:post]
I'm using IH 1.4.1
I needed to add the full list of parameters to my manage_create function - I have absolutely no idea why it doesn't use the existing permitted_params method.
#leave_request = LeaveRequest.new(params[:leave_request].permit(:user_id, :controller, :manager_id, :part_day, :comment, :selected_dates, :status, :leave_type_id, leave_dates_attributes:
[:id, :leave_request_id, :hours, :date_requested, :_destroy]))