Thinking Sphinx: search across multiple models: best practices? - ruby-on-rails

I want to add a jquery autocomplete with categories.
The request will search across multiples models (Forum topics, news, users...) with Thinking Sphinx
So in controller, I think it will look like that
def autocomplete
#news = Actu.search(params[:term]).map {|g| {:label => g.title, :category => "Actualités", :id => g.id}}
#topics = Topic.search(params[:term]).map {|g| {:label => g.title, :category => "Topics", :id => g.id}}
#anotherModel = ...
respond_to do |format|
format.js { render :json => #news+#topics+#anotherModel }
end
end
That working, but what do you think about these practice ?

You can try this awesome syntax
ThinkingSphinx.search 'pancakes', :classes => [Article, Comment]
Read more at http://freelancing-god.github.com/ts/en/searching.html

You can search across all indexed models in your application:
ThinkingSphinx.search(params[:term])
Then you can define for each model method, say autocomplete_json, that returns hash.
So, your action
def autocomplete
render :json => ThinkingSphinx.search(params[:term]).map(&:autocomplete_json)
end

Related

Pass one variable to next in JSON object in Rails 5

I need to be able to access user data in comments.
def index
#user = User.all
#libraries = Library.all.order('created_at ASC')
#comments = Comment.all
#user_likes = UserLike.all
render :json => #libraries, :include => [:user, :comments, :user_likes]
end
I tried this:
render :json => #libraries, :include => [:user, {:comments => :user}, :user_likes]
And it did not work. Any ideas?
Assuming you have the associations set up, you need to add include for the nested associations:
render :json => #libraries,
:include => {:user, {:comments => { include: :user}}, :user_likes}
When rendering JSON like this, Rails calls as_json first to convert the payload to hash. That's when the option include is used. You can check the documentation to see how it works.

Include associated model when rendering JSON in Rails

Right now I have this line:
render json: #programs, :except => [:created_at, :updated_at]
However, since a Program belongs_to a Company I would like to show the Company name instead of the Company Id.
How can I include the company name when rendering Programs?
Something like this should work:
render :json => #programs, :include => {:insurer => {:only => :name}}, :except => [:created_at, :updated_at]
i was getting the same "can't clone Symbol file" error while rendering json with includes from a controller method. avoided it like so:
render :json => #list.to_json( :include => [:tasks] )
You can also do this at the model level.
program.rb
def as_json(options={})
super(:except => [:created_at, :updated_at]
:include => {
:company => {:only => [:name]}
}
)
end
end
Now in your controller:
render json: #programs
Consider using jbuilder to include nested models in a maintainable way:
# /views/shops/index.json.jbuilder
json.shops #shops do |shop|
# shop attributes to json
json.id shop.id
json.address shop.address
# Nested products
json.products shop.products do |product|
json.name product.name
json.price product.price
end
end
Try this. Ref
#`includes` caches all the companies for each program (eager loading)
programs = Program.includes(:company)
#`.as_json` creates a hash containing all programs
#`include` adds a key `company` to each program
#and sets the value as an array of the program's companies
#Note: you can exclude certain fields with `only` or `except`
render json: programs.as_json(include: :company, only: [:name])
Also, no need to make #programs an instance variable, as I'm assuming we are not passing it to a view.
#includes is used to avoid n+1 query.
# http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Here is an example for the above example.Lets say you have posts and each post has many comments to it.
#posts = Post.where('id IN [1,2,3,4]').includes(:comments)
respond_to do |format|
format.json {render json: #posts.to_json(:include => [:comments]) }
end
#output data
[
{id:1,name:"post1",comments:{user_id:1,message:"nice"}}
{id:2,name:"post2",comments:{user_id:2,message:"okok"}}
{id:3,name:"post1",comments:{user_id:12,message:"great"}}
{id:4,name:"post1",comments:{user_id:45,message:"good enough"}}
]

How to get acts on taggable working

I am new to ruby on rails (and programming) and this is probably a really stupid question. I am using Rails 3.2 and trying to use acts_as_taggable_on to generate tags on articles and to have those tags show on article index and show pages as a clickable links.
I have tags clickable on both the article show and index pages, but the links just go back to the index page and don't sort according to the tag name. I have scoured the Internet and pieced together the code below from various sources, but I am clearly missing something.
Any help is greatly appreciated, as I have exhausted my seemingly limited knowledge! Thanks.
Here is what I have:
class ArticlesController < ApplicationController
def tagged
#articles = Article.all(:order => 'created_at DESC')
#tags = Article.tag_counts_on(:tags)
#tagged_articles = Article.tagged_with(params[:tags])
respond_to do |format|
format.html # index.html.erb
format.json { render :json => #articles }
end
end
def index
#articles = Article.paginate :page => params[:page], :per_page => 3
#tags = Article.tag_counts_on(:tags)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #articles }
end
end
module ArticlesHelper
include ActsAsTaggableOn::TagsHelper
end
class Article < ActiveRecord::Base
acts_as_ordered_taggable
acts_as_ordered_taggable_on :tags, :location, :about
attr_accessible :tag_list
scope :by_join_date, order("created_at DESC")
end
article/index.html.erb
<% tag_cloud(#tags, %w(tag1 tag2 tag3 tag4)) do |tag| %>
<%= link_to tag.name, articles_path(:id => tag.name) %>
<% end %>
article/show.html.erb
<%= raw #article.tags.map { |tag| link_to tag.name, articles_path(:tag_id => tag) }.join(" | ") %>
routes.rb file snippet
authenticated :user do
root :to => 'home#index'
end
devise_for :users
resources :users, :only => [:show, :index]
resources :images
resources :articles
You can run 'rake routes' from the terminal to see all your paths. Here your tags are pointing at articles_path, which you will see routes to the index action in the articles controller ("articles#index")
You could create another route in your routes.rb file, something like:
match 'articles/tags' => 'articles#tagged', :as => :tagged
Place it above others in the routes file if you want it to take precedence, and remember you can always run 'rake routes' in the terminal to see how the routes are interpreted.
see http://guides.rubyonrails.org/routing.html#naming-routes for more info (maybe read the whole thing)
Another (probably better) option would be to combine your desired functionality into the index action using params, e.g. .../articles?tagged=true. Then you could use logic to define the #articles variable in the index controller based on params[:tagged]. A simple example might be
def index
if params[:tagged]
#articles = Article.all(:order => 'created_at DESC')
else
Article.paginate :page => params[:page], :per_page => 3
end
#tags = Article.tag_counts_on(:tags)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #articles }
end
end
This is called DRYing up your code (for Don't Repeat Yourself); it would save you the need for code duplication in the articles#tagged action, which would make it easier to understand and maintain your code base.
Hope that helps.

Ordering by Sum from a separate controller part 2

Ok, so I had this working just fine before making a few controller additions and the relocation of some code. I feel like I am missing something really simple here but have spent hours trying to figure out what is going on. Here is the situation.
class Question < ActiveRecord::Base
has_many :sites
end
and
class Sites < ActiveRecord::Base
belongs_to :questions
end
I am trying to display my Sites in order of the sum of the 'like' column in the Sites Table. From my previous StackOverflow question I had this working when the partial was being called in the /views/sites/index.html.erb file. I then moved the partial to being called in the /views/questions/show.html.erb file and it successfully displays the Sites but fails to order them as it did when being called from the Sites view.
I am calling the partial from the /views/questions/show.html.erb file as follows:
<%= render :partial => #question.sites %>
and here is the SitesController#index code
class SitesController < ApplicationController
def index
#sites = #question.sites.all(:select => "sites.*, SUM(likes.like) as like_total",
:joins => "LEFT JOIN likes AS likes ON likes.site_id = sites.id",
:group => "sites.id",
:order => "like_total DESC")
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #sites }
end
end
I think it should be
#sites = #question.sites.all(:select => "sites.*, SUM(likes.like) as like_total",
:joins => "LEFT JOIN likes AS likes ON likes.site_id = sites.id",
:group => "sites.id",
:order => "SUM(likes.like) DESC")
Ah...turns out that I had move the #sites controller code from the SitesController to the QuestionsController Show action. I then had to change my partial in the /views/questions/show.html.erb page from
<%= render :partial => #question.sites %>
to
<%= render :partial => #sites %>

How to build a JSON response made up of multiple models in Rails

First, the desired result
I have User and Item models. I'd like to build a JSON response that looks like this:
{
"user":
{"username":"Bob!","foo":"whatever","bar":"hello!"},
"items": [
{"id":1, "name":"one", "zim":"planet", "gir":"earth"},
{"id":2, "name":"two", "zim":"planet", "gir":"mars"}
]
}
However, my User and Item model have more attributes than just those. I found a way to get this to work, but beware, it's not pretty... Please help...
Update
The next section contains the original question. The last section shows the new solution.
My hacks
home_controller.rb
class HomeController < ApplicationController
def observe
respond_to do |format|
format.js { render :json => Observation.new(current_user, #items).to_json }
end
end
end
observation.rb
# NOTE: this is not a subclass of ActiveRecord::Base
# this class just serves as a container to aggregate all "observable" objects
class Observation
attr_accessor :user, :items
def initialize(user, items)
self.user = user
self.items = items
end
# The JSON needs to be decoded before it's sent to the `to_json` method in the home_controller otherwise the JSON will be escaped...
# What a mess!
def to_json
{
:user => ActiveSupport::JSON.decode(user.to_json(:only => :username, :methods => [:foo, :bar])),
:items => ActiveSupport::JSON.decode(auctions.to_json(:only => [:id, :name], :methods => [:zim, :gir]))
}
end
end
Look Ma! No more hacks!
Override as_json instead
The ActiveRecord::Serialization#as_json docs are pretty sparse. Here's the brief:
as_json(options = nil)
[show source]
For more information on to_json vs as_json, see the accepted answer for Overriding to_json in Rails 2.3.5
The code sans hacks
user.rb
class User < ActiveRecord::Base
def as_json(options)
options = { :only => [:username], :methods => [:foo, :bar] }.merge(options)
super(options)
end
end
item.rb
class Item < ActiveRecord::Base
def as_json(options)
options = { :only => [:id, name], :methods => [:zim, :gir] }.merge(options)
super(options)
end
end
home_controller.rb
class HomeController < ApplicationController
def observe
#items = Items.find(...)
respond_to do |format|
format.js do
render :json => {
:user => current_user || {},
:items => #items
}
end
end
end
end
EDITED to use as_json instead of to_json. See How to override to_json in Rails? for a detailed explanation. I think this is the best answer.
You can render the JSON you want in the controller without the need for the helper model.
def observe
respond_to do |format|
format.js do
render :json => {
:user => current_user.as_json(:only => [:username], :methods => [:foo, :bar]),
:items => #items.collect{ |i| i.as_json(:only => [:id, :name], :methods => [:zim, :gir]) }
}
end
end
end
Make sure ActiveRecord::Base.include_root_in_json is set to false or else you'll get a 'user' attribute inside of 'user'. Unfortunately, it looks like Arrays do not pass options down to each element, so the collect is necessary.
Incase anyone is looking for an alternative solution for this, this is how I solved this in Rails 4.2:
def observe
#item = some_item
#user = some_user
respond_to do |format|
format.js do
serialized_item = ItemSerializer.new(#item).attributes
serialized_user = UserSerializer.new(#user).attributes
render :json => {
:item => serialized_item,
:user => serialized_user
}
end
end
end
This returns the serialized version of both objects as JSON, accessible via response.user and response.item.
There are a lot of new Gems for building JSON now, for this case the most suitable I have found is Jsonify:
https://github.com/bsiggelkow/jsonify
https://github.com/bsiggelkow/jsonify-rails
This allows you to build up the mix of attributes and arrays from your models.
Working answer #2 To avoid the issue of your json being "escaped", build up the data structure by hand, then call to_json on it once. It can get a little wordy, but you can do it all in the controller, or abstract it out to the individual models as to_hash or something.
def observe
respond_to do |format|
format.js do
render :json => {
:user => {:username => current_user.username, :foo => current_user.foo, :bar => current_user.bar},
:items => #items.collect{ |i| {:id => i.id, :name => i.name, :zim => i.zim, :gir => i.gir} }
}
end
end
end

Resources