Rails: Sunspot text searching with model associations, using :through - ruby-on-rails

How do I search with associations and through with sunspot?
class StaticController < ApplicationController
def search
#search = Sunspot.search Business, Service do
fulltext params[:q]
paginate :per_page => 10
order_by_geodist(:location, *Geocoder.coordinates(params[:loc]))
end
#biz = #search.results
end
class Business < ActiveRecord::Base
attr_accessible :name
has_many :services, :through => :professionals
searchable do
text :name #name in business column
# how to do I get the services?
end
end
class Service < ActiveRecord::Base
attr_accessible :service
belongs_to :professional
end
class Professional < ActiveRecord::Base
belongs_to :business
has_many :services, as: :servicable
end
In the view, I have this (lots of looping)
<%= #biz.each do |b| %>
<%= b.name %>
<!-- looping through professionals model -->
<% b.professionals.each do |prof| %>
<!-- looping through services model -->
<% prof.services.each do |s| %>
<%= s.service %>
<% end %>
<% end %>
<% end %>
This works if I search for a name that is within the business model, but what if I'm searching through a term that's in the Service model? It won't display correctly because my view is only coming from the business side. How do I make it so the business name will pop up if I search through Service model?
Thanks

You will need to make additional indexes for the associated models in the calling model to make this happen. For example:
class Business < ActiveRecord::Base
attr_accessible :name
has_many :services, :through => :professionals
searchable do
text :name #name in business column
text :services do # this one for full text search
services.map(&:service).compact.join(" ")
end
string :services , :multiple => true do #this one for exact searches
services.map(&:service).compact
end
end
end
After that you can do queries like:
Bussines.search do
with(:services, "some_service")
end.execute.results
Now you no longer have to do join on mysql tables to fetch data. You can just fetch data from the solr. This is one of biggest advantages of solr.
I hope this makes it clear. Fell free to drop a comment if you need more details.

Related

Rails Invoicing App

I want to create an invoice in rails. Invoice can have items and each item will have quantity, tax & price. It's a typical invoice we see everyday.
In order to create an invoice what is the best approach.
What is the common model for invoice and items?
I know Items will be a separate model. But how can we have one view for invoice, which creates both the invoice and items added to it?
What I mean is, Inside a new invoice page, there will be list of the clients, and list of the items , But here i'm not sure how to make the association when i create invoice. Is there any good example that i can follow ?
Please I'd appreciate some Help. Or even just a walk through of the steps i need to follow in order to accomplish that...
Here's my basic ERD
Quite a broad question, here's what I'd do:
#app/models/invoice.rb
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :line_items
has_many :items, through: :line_items
accepts_nested_attributes_for :line_items
end
#app/models/line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :invoice
belongs_to :item
end
#app/models/item.rb
class Item < ActiveRecord::Base
belongs_to :company
has_many :line_items
has_many :invoices, through: :line_items
end
--
#app/models/user.rb
class User < ActiveRecord::Base
has_many :invoices
end
This will be the base level "invoice" association structure - your clients/users can be built on top of it.
Your routes etc can be as follows:
#config/routes.rb
resources :invoices
#app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def new
#invoice = current_user.invoices.new
#invoice.line_items.build
end
def create
#invoice = current_user.invoices.new invoice_params
#invoice.save
end
end
Then your view will be something like this:
#app/views/invoices/new.html.erb
<%= form_for #invoice do |f| %>
<%= f.fields_for :line_items do |l| %>
<%= f.text_field :quantity %>
<%= f.collection_select :product_id, Product.all, :id, :name %>
<% end %>
<%= f.submit %>
<% end %>
This would create the corresponding #invoice, with which you'll be able to call as follows:
#user.invoices.first
Apart from this, I don't have anywhere enough specific information to help specifically
May I recommend using the payday gem? I have created invoice models in the past applications and I'll tell you what, it can get pretty tricky sometimes depending on the type of application you're building. But the reason I like using this gem besides the convenience factor is that it can also render your invoices as a customizable PDF.
It makes adding items to the invoice a breeze as well, for example from their GitHub page:
invoice = Payday::Invoice.new(:invoice_number => 12)
invoice.line_items << Payday::LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
invoice.line_items << Payday::LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
invoice.line_items << Payday::LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
invoice.render_pdf_to_file("/path/to_file.pdf")

Rails has_many relationship with prefilled views

I have a pretty basic Rails 4 app, and am using Cocoon's nested forms to manage the has_many... :through model association.
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
# ... etc
end
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
# ... etc
end
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluation, reject_if: :all_blank
# ... etc
end
When I use Cocoon in the View, I want to use the New Assessment view to pre-fill all the Student records in order to create a new Evaluation for each one. I don't want to have to do some hacky logic on the controller side to add some new records manually, so how would I structure the incoming request? With Cocoon I see that requests have some number in the space where the id would go (I've replaced these with ?? below).
{"utf8"=>"✓", "authenticity_token"=>"whatever", "assessment"=>{"description"=>"quiz 3", "date(3i)"=>"24", "date(2i)"=>"10", "date(1i)"=>"2015", "assessments_attributes"=>{"??"=>{"student_id"=>"2", "grade" => "A"}, "??"=>{"student_id"=>"1", "grade" => "B"}, "??"=>{"student_id"=>"3", "grade"=>"C"}}, }}, "commit"=>"Create Assessment"}
I see in the Coccoon source code that this is somehow generated but I can't figure out how it works with the Rails engine to make this into a new record without an ID.
What algorithm should I use (or rules should I follow) to fill in the id above to make a new record?
"??"
Never a good sign in your params.
With Cocoon I see that requests have some number in the space where the id would go
That ID is nothing more than the next ID in the fields_for array that Rails creates. It's not your record's id (more explained below).
From your setup, here's what I'd do:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
end
#app/models/evaluation.rb
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
end
#app/models/assessment.rb
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluations, reject_if: :all_blank
end
This will allow you to do the following:
#app/controllers/assessments_controller.rb
class AssessmentsController < ApplicationController
def new
#assessment = Assessment.new
#students = Student.all
#students.each do
#assessment.evaluations.build
end
end
end
Allowing you:
#app/views/assessments/new.html.erb
<%= form_for #assessment do |f| %>
<%= f.fields_for :evaluations, #students do |e| %>
<%= e.hidden_field :student_id %>
<%= e.text_field :grade %>
<% end %>
<%= f.submit %>
<% end %>
As far as I can tell, this will provide the functionality you need.
Remember that each evaluation can connect with existing students, meaning that if you pull #students = Student.all, it will populate the fields_for accordingly.
If you wanted to add new students through your form, it's a slightly different ballgame.
Cocoon
You should also be clear about the role of Cocoon.
You seem like an experienced dev so I'll cut to the chase - Cocoon is front-end, what you're asking is back-end.
Specifically, Cocoon is meant to give you the ability to add a number of fields_for associated fields to a form. This was discussed in this Railscast...
Technically, Cocoon is just a way to create new fields_for records for a form. It's only required if you want to dynamically "add" fields (the RailsCast will tell you more).
Thus, if you wanted to just have a "static" array of associative data fields (which is I think what you're asking), you'll be able to use fields_for as submitted in both Max and my answers.
Thanks to #rich-peck I was able to figure out exactly what I wanted to do. I'm leaving his answer as accepted because it was basically how I got to my own. :)
assessments/new.html.haml (just raw, no fancy formatting)
= form_for #assessment do |f|
= f.fields_for :evaluations do |ff|
.meaningless-div
= ff.object.student.name
= ff.hidden_field :student_id, value: ff.object.student_id
= ff.label :comment
= ff.text_field :comment
%br/
assessments_controller.rb
def new
#assessment = Assessment.new
#students = Student.all
#students.each do |student|
#assessment.evaluations.build(student: student)
end
end

How to optimize this code in Ruby on Rails?

I have 3 models source.rb belongs to category.rb and feed_entry.rb belongs to source.rb.
I need to display feed_entries in category
Category name
FeedEntry 1
FeedEntry 2
FeedEntry 3
Now it looks like this
class CategoriesController < ApplicationController
def show
#category = Category.find(params[:id])
#sources = #category.sources.all
end
end
show.html.erb
<%= #category.name %></h4>
<% #sources.each do |source| %>
<% source.feed_entries.each do |feed_entry| %>
<%= link_to feed_entry.name, feed_entry %>
<%= feed_entry.source.title %>
<% end %>
<% end %>
this is very slow
I use mongoid 4, rails 4
Models
class Category
include Mongoid::Document
field :name, type: String
has_many :sources, dependent: :destroy
end
class FeedEntry
include Mongoid::Document
field :name, type: String
belongs_to :source, touch: true
validates :source_id, presence: true
end
class Source
include Mongoid::Document
field :title, type: String
has_many :feed_entries, dependent: :destroy
belongs_to :category, touch: true
end
Some thinks to know :
Never use .all, unless you know size of result data. Always use pagination or limit.
When you have a loop like your each in view, this will call queries like this :
Give me a category
Give me its sources
Give me feed entries for source 1
Give me feed entries for source 2
....
You should eagler load your association like this :
#sources = #category.sources.limit(20).includes(:feed_entries)
It will to theses queries :
Give me a category
Give me its sources
Give me feed entries for theses sources
If you don't want any information about categories (like I think), you should add a relation to your model :
Class Category
has_many :sources
has_many :feed_entries, :through => :sources
end
Then call in your controller
#feed_entries = #category.feed_entries
This will do only ONE query :
Give me category
Give me the feed entries of the category
That's it !
I found a solution:
In Category.rb add feed_entries
class Category
def feed_entries
FeedEntry.in(source_id: sources.map(&:id))
end
end
and in show.html.erb
<% #category.feed_entries.includes(:source).each do |feed_entry| %>
<%= link_to feed_entry.name, feed_entry %>
<%= feed_entry.source.title %>
<% end %>

Display number of items per category with .count rails

I'm trying to display the number of photos a category has in a list.
For it, I'm trying to do in my view:
<%= #photos.zone.name("Zone1").count%>
<%= #photos.zone.name("Zone2").count%>
<%= #photos.zone.name("Zone3").count%>
<%= #photos.zone.name("Zone4").count%>
But this doesn't work, and I don't know if this will make a million requests to my ddbb.
Which is the correct way to do this? Making a scope for each category?
Update
class Photo < ActiveRecord::Base
# validate :validate_minimum_image_size
has_many :tags , dependent: :destroy
belongs_to :user
belongs_to :category
belongs_to :zone
validates_presence_of :title, :description, :category, :zone
acts_as_votable
is_impressionable
before_destroy { |record| record.tags.destroy_all if record.tags.any? }
class User < ActiveRecord::Base
rolify
has_many :photos
has_many :tags, through: :photos
Thanks
This line here is wrong because you are calling it on a collection, and it doesn't represent your data model correctly.
<%= #photos.zone.name("Zone1").count%>
Let's say you wanted to get the zone of each photo, you would do something like this:
<% #photos.each do |photo| %>
<%= photo.zone.count %>
<% end %>
This still does not make sense because your association states that a photo belongs to a zone. So a photo can only have one zone per your data model.
class Photo
# ...
belongs_to :zone
end
Based on this information, I will assume that you want to display zones, and the number of photos per zone. In which case you would do something like this:
<%= #zone.photos.count %>
Or, if you wanted to show multiple zones on the same page:
<% #zones.each do |zone| %>
<%= zone.photos.count %>
<% end %>
How would you prepare the data? In your controller you would do something like this:
#zone = Zone.includes(:photos).find(params[:id]) # assuming a /zones/:id path
Or for multiple zones:
#zones = Zone.all.includes(:photos) # assuming a /zones/ path
It is also possible that you want to display photos grouped by zone, which is another story.
Extending #Mohamad's answer
The best bet is to use counter_cache.In your Photo model set counter_cahe =>true for zone
class Photo < ActiveRecord::Base
# validate :validate_minimum_image_size
has_many :tags , dependent: :destroy
belongs_to :user
belongs_to :category
belongs_to :zone,:counter_cache => true #here
validates_presence_of :title, :description, :category, :zone
acts_as_votable
is_impressionable
before_destroy { |record| record.tags.destroy_all if record.tags.any? }
end
Then add a column photo_counts to your zones table and use it like this
<% #zones.each do |zone| %>
<%= zone.photo_counts %>
<%end%>
This avoids multiple requests to the DB.Look into this Railscast for more info.
Hope it helps!
simply
<%= #photos.select{|photo| photo.zone.name == "Zone1"}.count%>

Understanding associations in rails 3

Seems I need to brush up on my associations in rails. At present I am trying to display all posts that have the department name as staff.
two models exist at present, posts and departments
class Post < ActiveRecord::Base
belongs_to :department
attr_accessible :title, :comments, :department_id
end
class Department < ActiveRecord::Base
has_many :posts
attr_accessible :name, :post_id
#Scopes
scope :staff_posts, where(:name => "Staff")
end
So i want to display all posts that have the department name staff
to do this i have put this in my controller
class PublicPagesController < ApplicationController
def staffnews
#staffpost = Department.staff_posts
end
end
In my view i am trying to display all these posts like so
<% #staffpost.each do |t| %>
<h2><%= t.title %>
<h2><%= t.comments %></h2>
<% end %>
Clearly going wrong somewhere as i get undefined method nil, even though i have 3 posts with the name 'Staff'
Can someone please explain where i am misunderstanding the association as would love to get this right
EDIT
Routes
scope :controller => :public_pages do
get "our_news"
match "our_news/staffnews" => "public_pages#staffnews"
In controller it returns department with name staff. And you are using title and comments on on department objects thats why its giving nil method error.
Use like this:
def staffnews
#dept_staff = Department.staff_posts
end
<% #dept_staff.each do |ds| %>
<% ds.posts.each do |p| %>
<h2><%= p.title %></h2>
<h2><%= p.comments %></h2>
<% end %>
<% end %>
or
In post model create named_scope
class Post < ActiveRecord::Base
belongs_to :department
attr_accessible :title, :comments, :department_id
scope :staff_posts, :include => :department, :conditions => {"departments.name" => "Staff"}
end
class Department < ActiveRecord::Base
has_many :posts
attr_accessible :name, :post_id
end
Controller:
def staffnews
#staffpost = Post.staff_posts
end
View: #No change
<% #staffpost.each do |t| %>
<h2><%= t.title %></h2>
<h2><%= t.comments %></h2>
<% end %>
Your staff_posts scope is only selecting the Departments with the name "Staff". Assuming you will have one and only one department named staff, you have a few ways to handle this.
This will find all departments with the name staff, and eager load the posts that go along with it:
#department = Department.where(name: "Staff").include(:posts).first
Since you are trying to scope Post, however, this belongs in Post. Here's an example using a method as scope:
class Post < ActiveRecord::Base
belongs_to :department
attr_accessible :title, :comments, :department_id
def self.staff
where(department_id: staff_department_id)
end
def staff_department_id
Department.find_by_name!("Staff").id
end
end
This way, you can use #staff_posts = Post.staff and iterate over that collection (Note: I don't recommend getting staff_department_id this way permanently. This could be set to a constant when the app boots up, or some other more robust solution).
You can find the all the posts that have the department name staff by following changes:
class PublicPagesController < ApplicationController
def staffnews
#get all the department which have name is staff
departments = Department.where("name=?","staff")
#get all the ids
department_ids = departments.map(&:id)
#retrieve post that department name is staff
#staffpost = Post.find_by_department_id(department_ids)
end
end

Resources