How to reuse RABL partial templates by keeping conventions? - ruby-on-rails

I am using Ruby on Rails 4 and the RABL gem. I would like to reuse templates by rendering the show.json.rabl view from the index.json.rabl and keep convention stated at jsonapi.org (source: RABL documentation).
That is, I have the following files:
# index.json.rabl
collection #articles => :articles
attributes :id, :title, :...
# show.json.rabl
collection [#article] => :articles
attributes :id, :title, :...
Since for each article instance the index.json.rabl renders the same attributes as for the show.json.rabl I would like to reuse the latter as partial template, or (maybe) to extend the first.
What I would like to output is:
# index JSON output
{"articles":[{"id":1,"title":"Sample 1","...":...},{"id":2,"title":"Sample 2","...":...},{"id":3,"title":"Sample 3","...":...}]}
# show JSON output
{"articles":[{"id":1,"title":"Sample 1","...":...}]}

Here is a neat way:
things/base.rabl:
attributes :id, :title
child(:another_child, :object_root => false) { attributes :id, :title }
things/show.rabl:
object #thing
extends 'things/base'
things/index.rabl:
collection #things
extends 'things/base'

You can use extends - https://github.com/nesquena/rabl/wiki/Reusing-templates
object #post
child :categories do
extends "categories/show"
end

I was having trouble interpreting what I needed to do from https://github.com/nesquena/rabl/wiki/Reusing-templates but finally figured it out. For me, I had files structured like this: (names changed to protect the innocent ;) )
/app/controllers/api/my/boring_things_controller.rb
/app/views/api/my/boring_things/index.json.rabl
/app/controllers/api/my/interesting_things_controller.rb
/app/views/api/my/interesting_things/index.json.rabl
boring_things/index.json.rabl looked like this (except for real, it had a LOT more attributes):
collection #boring_things, :root => :things, :object_root => false
attributes :id,
:name,
:created_at,
:updated_at
And interesting_things/index.json.rabl looked ALMOST identical:
collection #interesting_things, :root => :things, :object_root => false
attributes :id,
:name,
:created_at,
:updated_at
I wanted to reuse just the attribute parts. The confusing part to me about https://github.com/nesquena/rabl/wiki/Reusing-templates was that I didn't really think I needed a 'node', and I didn't think I needed 'child' either because I wanted the attributes at the top level, not as a child object. But it turns out I did need 'child'. Here's what I ended up with:
/app/controllers/api/my/boring_things_controller.rb
/app/views/api/my/boring_things/index.json.rabl
/app/controllers/api/my/interesting_things_controller.rb
/app/views/api/my/interesting_things/index.json.rabl
/app/views/api/my/shared/_things.json.rabl
_things.json.rabl is this:
attributes :id,
:name,
:created_at,
:updated_at
boring_things/index.json.rabl is now this:
child #boring_things, :root => :things, :object_root => false do
extends "api/my/shared/_things"
end
and interesting_things/index.json.rabl is now this:
child #interesting_things, :root => :things, :object_root => false do
extends "api/my/shared/_things"
end
So for you, instead of rendering show from index, I'd try doing something where you extract article attributes out into a rabl file shared by index and show. In the same directory where your show.json.rabl and index.json.rabl are (I'm assuming it's views/articles/index.json.rabl and views/articles/show.json.rabl), create a "_article_attributes.json.rabl" file. Sorry I don't have your exact setup so I can't try it out myself syntactically, but it should be something like this in your index.json.rabl:
child #articles do
extends "articles/_article_attributes"
end
[A side note: Another thing that tripped me up when I was doing this was that since my shared file was in a sibling-directory to the different rabl files that used them and I was trying to use a relative path and that did NOT work. Instead I had to use the path starting with whatever's under "app/views" (i.e. "extends 'api/my/shared/_things'" not "extends '../shared/_things'"). That was a weird situation and I won't go into why we did it that way, but if you can it's better to have the shared file in the same directory as your index and show, like you're doing.]

Related

Obfuscation of URLs - rails

I'm working on a project that's left me stumped, hoping someone out there might have some interesting input. I see there are several gems available for obfuscation of urls, but they seem to stop at the slug level instead of the controller level - i.e. www.foo.com/mycontroller/8sZ16lp. I'm looking for a method to produce something along the lines of www.foo.com/8asd31Ud, dropping the controller name. I checked the docs of the obsufacate_id gem, but it doesn't appear to go that far.
To give more background - I'm really hoping to have www.foo.com/mycontroller/15/edit = www.foo.com/95Ali32
As this doesn't match the convention of rails RESTful URL's my guess is you'll likely just need to write your own routes and maybe more. Out of curiosity, how would you envision that the system knows what type of object to load with "95Ali32"? This might be better to build with Sinatra or something that gives you more control over the routing and less convention.
Here's one possible approach that uses a table with your slugs that maps to type and object id:
# migration
create_table :slugs do |t|
t.string :object_type, :null => false
t.string :object_id, :null => false
t.string :slug
t.timestamps
end
# models
class Slugs < ActiveRecord::Base
belongs_to :object, :polymorhic => true
end
class AModel < ActiveRecord::Base
has_one :slug, :as => :owner
end
# routes.rb
# note that anything else should go above this because this will catch all other URL's
get '*slug', to: 'slugs#show'
# controller
class SlugsController < ApplicationController
def show
#object = Slug.where(slug: params[:slug])
raise ActiveRecord::NotFound.new unless #object
render #object.kind
end
end
You would then need to build views for each type of object. See this related question
Update
Here's another idea. How obscure do you need the slug? What if each model has a known code and then the ID is encoded some way and appended to the model code? Then you could do something much simpler in the code using routes that are preconfigured:
# You could generate this too, or just hard-code them
prefixes = ['8sZ', '95Ali']
[:a_model, :another_model].each do |model|
match "#{prefixes.pop}:id", :controller => model.to_s.underscore.pluralize, :action => :show, :as => model
end
This would give you routes like these
/8sZ1 #=> AModelsController#show(:id => 1)
/95Ali341 #=> AModelsController#show(:id => 341)
You could take this another level and use friendly_id to generate slugs for the model ID's. Or use UUID's instead of integer ID's (supported by PostgreSQL).

How to pass a parameter inside a simple_form group_method?

I'm trying to display active projects per party in a drop down list. active_projects is a method within the Party model. The grouped_collection_select code below works however, when I attempt to convert my form into a simple_form, my active_projects method is no longer recognised.
Below are my two code extracts. The first working correctly while the other causes an error.
# rails default
<%= f.grouped_collection_select(:project_id,
Party.all,
:"active_projects(#{date.strftime("%Y%m%d")})",
:party_name,
:id, :project_name) %>
# simple form
<%= f.input :project_id,
collection: Party.all, as: :grouped_select,
group_method: :"active_projects(#{date})" %>
I know this one is a little old but I have a solution to this problem using simple_form. I am not sure if it is the best solution but it does work.
Basically, the issue comes down to passing in a value to the group_method. In my case I had a class that needed to get the current_users company that he/she belongs to. My model/database structure was like this:
Type -> Category
In my case the Type records were global and did not belong to a specific company. However, the category model records did belong to a specific company. The goal is to show a grouped select with global types and then company-specific categories underneath them. Here is what I did:
class Type < ActiveRecord::Base
has_many :categories
attr_accessor :company_id
# Basically returns all the 'type' records but before doing so sets the
# company_id attribute based on the value passed. This is possible because
# simple_form uses the same instance of the parent class to call the
# group_by method on.
def self.all_with_company(company_id)
Type.all.each do |item|
item.company_id = company_id
end
end
# Then for my group_by method I added a where clause that reuses the
# attribute set when I originally grabbed the records from the model.
def categories_for_company
self.categories.where(:company_id => self.company_id)
end
end
So the above is a definition of the type class. For reference here is my definition of the category class.
class Category < ActiveRecord::Base
belongs_to :company
belongs_to :type
end
Then on my simple_form control I did this:
<%= f.association :category, :label => 'Category', :as => :grouped_select, :collection => Type.all_with_company(company_id), :group_method => :categories_for_company, :label_method => :name %>
Basically instead of passing in the value we want to filter on in the :group_method property we pass it in on the :collection property. Even though it will not be used to get the parent collection it is just being stored for later use in the class instance. This way, when we call another method on that class it has the value we need to do our filtering on the child.

How to use reform gem with two models that share identical column names

I am using the Reform gem and want to create 2 objects (an instance of Foo, and one of Bar) which both have a 'name' attribute:
class MarflarForm < Reform:Form
include DSL
include Reform::Form::ActiveRecord
property :name, on: :foo
property :name, on: :bar
end
But I cant do this for obvious reasons:
= simple_form_for #form do |f|
= f.input :file
= f.input :file
The only way I can think of getting round this is by renaming one of database columns to 'title'. Is there another way?
Its just simple, For achieve this goal please use:
class MarflarForm < Reform:Form
include DSL
include Reform::Form::ActiveRecord
property :name, on: :foo
property :name, on: :bar
end
and In the view
= simple_form_for #form do |f|
= f.input "foo[name]"
= f.input "bar[name]"
This avoid name collision.
I've never used the Reform gem, but it looks tome like you can call the properties whatever you want. So try
property :foo_file, on: :foo
property :baz_file, on: :baz
Then, on save, you'll just have to be responsible for mapping those properties back to the correct model attributes.
#form.save do |data, nested|
#foo.file = nested[:foo_file]
#baz.file = nested[:baz_file]
# etc...
end
Does something like that work?

ActiveAdmin, polymorphic associations, and custom filters

Rails 3.1, ActiveAdmin 0.3.4.
My question is somewhat similar to this one but different enough in terms of data modeling that I think it warrants its own response. Models:
class CheckoutRequest < ActiveRecord::Base
has_one :request_common_data, :as => :requestable, :dependent => :destroy
end
class RequestCommonData < ActiveRecord::Base
belongs_to :requestable, :polymorphic => true
end
The RequestCommonData model has a completed field (boolean) that I'd like to be able to filter in ActiveAdmin's CheckoutRequest index page. I've tried a few different approaches to no avail, including the following:
filter :completed, :collection => proc { CheckoutRequest.all.map { |cr| cr.request_common_data.completed }.uniq }
which results in no filter being displayed. Adding :as => :select to the line, as follows:
filter :completed, :as => :select, :collection => proc { CheckoutRequest.all.map { |cr| cr.request_common_data.completed }.uniq }
results in the following MetaSearch error message:
undefined method `completed_eq' for #<MetaSearch::Searches::CheckoutRequest:0x007fa4d8faa558>
That same proc returns [true, false] in the console.
Any suggestions would be quite welcome. Thanks!
From the meta_search gem page you can see that for boolean values the 'Wheres' are:
is_true - Is true. Useful for a checkbox like “only show admin users”.
is_false - The complement of is_true.
so what you need is to change the generate input name from 'completed_eq' to be 'completed_is_true' or 'completed_is_false'.
The only way I have found this possible to do is with Javascript, since by looking at the Active Admin code, the 'Wheres' are hardcoded for each data type.
I would usually have a line like this in my activeadmin.js file (using jQuery)
$('#q_completed_eq').attr('name', 'q[completed_is_true]');
or
$('#q_completed_eq').attr('name', 'q[completed_is_false]');
Terrible and ugly hack but have found no other solution myself.
Be careful to enable this only in the pages you want.
--- NEW FOR VERSION 0.4.2 and newer ---
Now Active Admin uses separate modules for each :as => ... option in the filters.
So for example you can place the code below inside an initializer file
module ActiveAdmin
module Inputs
class FilterCustomBooleanInput < ::Formtastic::Inputs::SelectInput
include FilterBase
def input_name
"#{#method}_is_true"
end
def input_options
super.merge(:include_blank => I18n.t('active_admin.any'))
end
def method
super.to_s.sub(/_id$/,'').to_sym
end
def extra_input_html_options
{}
end
end
end
end
and the use
:as => :custom_boolean
where you specify your filter.

Rabl Multi-model collection

I am using RABL to output a Sunspot/SOLR result set and the search results object is composed of multiple model types. Currently within the rabl view I have:
object false
child #search.results => :results do
attribute :id, :resource, :upccode
attribute :display_description => :description
code :start_date do |r|
r.utc_start_date.to_i
end
code :end_date do |r|
r.utc_end_date.to_i
end
end
child #search => :stats do
attribute :total
end
The above works for a single model; however, when multiple model types are within the #search.results collection, it fails because both classes do not have the same instance methods. Does anyone know how to have different attributes based on the type? Ultimately, it would be nice to conditionally extend a different template within the results collection based on the type of object. Something like the below pseudo code:
child #search.results => :results do |r|
if r.class == Product
extends "product/base"
else
extends "some other class base"
end
end
You can take full control with 'node' and avoid this issue entirely in the 'worst' case:
node :results do
#search.results.map do |r|
if r.is_a?(Product)
partial("product/base", :object => r)
else # render other base class
partial("other/base", :object => r)
end
end
end
Does that help?

Resources