activerecord - getting the most popular category - ruby-on-rails

I have a model Category and class method tickets_num which return the number of tickets that belongs to this category:
Category.first.tickets_num # => 2
class Category < ActiveRecord::Base
has_many :tickets
def tickets_num
self.tickets.count
end
end
I would like to do controller method "popular" which will return me 3 categories with the highest number of tickets_num , how can I do it in the most elegant way?
def popular
#categories = Category.order(tickets_num).all.limit(3)
end
my method doesn't work.

The following active record query should get you what you need. Not sure if there is a cleaner way to do that.
Category.select("categories.*, COUNT(tickets.id) AS t_count").joins(:tickets).group("categories.id").order("t_count DESC").limit(3)

Related

Is overriding an ActiveRecord relation's count() method okay?

Let's say I have the following relationship in my Rails app:
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
end
I want parents to be able to see a list of their chatty kids, and use the count in paginating through that list. Here's a way to do that (I know it's a little odd, bear with me):
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
proxy_association.owner.kids.where(:chatty => true)
end
end
end
But! Some parents have millions of kids, and p.kids.for_chatting.count takes too long to run, even with good database indexes. I'm pretty sure this cannot be directly fixed. But! I can set up a Parent#chatty_kids_count attribute and keep it correctly updated with database triggers. Then, I can:
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
parent = proxy_association.owner
kid_assoc = parent.kids.where(:chatty => true)
def kid_assoc.count
parent.chatty_kids_count
end
end
end
end
And then parent.kids.for_chatting.count uses the cached count and my pagination is fast.
But! Overriding count() on singleton association objects makes the uh-oh, I am being way too clever part of my brain light up big-time.
I feel like there's a clearer way to approach this. Is there? Or is this a "yeah it's weird, leave a comment about why you're doing this and it'll be fine" kind of situation?
Edit:
I checked the code of will_paginate, seems like it is not using count method of AR relation, but i found that you can provide option total_entries for paginate
#kids = #parent.kids.for_chatting.paginate(
page: params[:page],
total_entries: parent.chatty_kids_count
)
This is not working
You can use wrapper for collection like here
https://github.com/kaminari/kaminari/pull/818#issuecomment-252788488​,
just override count method.
class RelationWrapper < SimpleDelegator
def initialize(relation, total_count)
super(relation)
#total_count = total_count
end
def count
#total_count
end
end
# in a controller:
relation = RelationWrapper.new(#parent.kids.for_chatting, parent.chatty_kids_count)

Rails 4 Create Associated Object on Save

How can I create multiple associated objects automatically just after I save a new primary object?
For example
In Rails 4, I have three objects: Businesses, Budgets, and Categories.
#app/models/business.rb
class Business < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
#app/models/budget.rb
class Budget < ActiveRecord::Base
#attrs id, business_id, department_id, value
belongs_to :business
belongs_to :category
end
#app/models/category.rb
class Category < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
When I create a new Business, after saving the new Business, I would like to atomically create a Budget for each Category and give it value of $0. This way, when I go to show or edit a new Business, it will already have the associated Categories and Budgets, which can then be edited. Thus, upon creating a new Business, multiple new Budgets will be created, one for each Category, each with the value of 0.
I read this article: Rails 3, how add a associated record after creating a primary record (Books, Auto Add BookCharacter)
And I am wondering if I should use the after_create callback in the Business model and have the logic then exist in the Budgets controller (not exactly sure how to do this) or if I should add logic to the businesses_controller.rb in the 'new' call with something similar to:
#business = Business.new
#categories = Category.all
#categories.each do |category|
category.budget.build(:value => "0", :business_id => #business.id)
end
In my experience, it's best to avoid using callbacks unless it relates to a given model's persistence. In this case, letting a budget set it's own default value when one isn't supplied is good use of a callback. That also removes some complexity from your logic.
class Budget
before_validate :set_value
...
private
def set_value
self.value ||= 0
end
end
For the rest, I would create custom classes, each with a single responsibility, to systematically generate a new business. Here's an example. Keep in mind that this is not meant to be copy and pasted, it's just to illustrate a concept:
class BusinessGenerator < Struct.new(:business_params)
attr_reader :business
def generate
create_business
create_budgets
end
private
def create_business
#business = Business.create!(business_params)
end
def create_budgets
BudgetGenerator.new(#business).create
end
end
class BudgetGenerator < Struct.new(:business)
def generate
categories.each do |c|
business.budgets.create!(category: c)
end
end
private
def categories
Category.all
end
end
This is nice because it separates concerns and is easily extensible, testable and doesn't use Rails magic like accepts_nested_attributes_for. For example, if in the future you decide that not all businesses need a budget in every category, you can easily pass the ones you want as an argument to BudgetGenerator.
You'll instantiate the BusinessGenerator class in the controller:
class BusinessController < ActionController::Base
...
def create
generator = BusinessGenerator.new(business_params)
if generator.generate
flash[:success] = "Yay"
redirect_to generator.business
else
render :new
end
end
...
end
Some sticking points you might have with this approach include:
Returning validation errors to your business form
If the creation of a budget fails, you're stuck with a budget-less business. You can't wait to save business until after the budgets are created because there is no id to associate. Perhaps consider putting a transaction inside of the generator method.
Regardless of Brent Eicher's great advice, I've never experienced anything bad from using callbacks. If you don't mind using them, you could do the following (if you're setting the budget at 0 each time):
#app/models/business.rb
class Business < ActiveRecord::Base
before_create :build_budgets
private
def build_budgets
Category.all.each do |category|
self.budgets.build(category: category, value: "0")
end
end
end
--
Also, you need to make sure your budget foreign keys are correct.
I see you have department_id when Budget belongs_to Category. You should make this category_id or define the foreign_key:
#app/models/budget.rb
class Budget < ActiveRecord::Base
belongs_to :category, foreign_key: "department_id"
end
I ended up adding the logic to the create method in the Business controller to loop through all Categories and create a budget just after save. Note that I was lazy and didn't put in any error handling. :
def create
#business = Business.new(params[:business])
#results = #business.save
#categories = Categories.all
#categories.each do |category|
category.budgets.create(:amount => "0", :business_id => #business.id)
end
respond_to do |format|
...
end
end

Multiple models in controller, what good practices with rails 4?

I'm currently developing a web application using Ruby on Rails. I have a controller which get two parameters :city and :sub_category.
I've a model named InterestPoint which belongs to a SubCategory and a City
Using what I read on the web, I end up doing this to list all InterestPoint matching the city and the category :
class SubCategoryController < ApplicationController
def index
#city = City.where(route_str: params[:city]).first
#sub_category = SubCategory.where(route_str: params[:sub_category]).first
#interest_point = InterestPoint.where(city_id: #city.id).where(sub_category_id: #sub_category.id)
end
end
My models :
class SubCategory < ActiveRecord::Base
has_many :interest_points
end
class City < ActiveRecord::Base
has_many :interest_points
end
class InterestPoint < ActiveRecord::Base
belongs_to :sub_category
belongs_to :city
end
Here is the route concerned :
get '/:city/:sub_category/' => 'sub_category#index'
It's running well, but RubyMine (my IDE) keeps telling me this is a bad practice : Only one model per controller should be used.
So, my question is, why is it a bad practice, and how can I do it "the right way" ?
My answer based on khaled_gomaa one :
Step 1, moved from SubCategory Controller to InterestPoint Controller since it's what the page is about.
Step 2, created scopes inside my InterestPoint Model :
scope :by_location, lambda { |str|
joins(:city).where('cities.route_str = ?', str)
}
scope :by_category, lambda { |str|
joins(:sub_category).where('sub_categories.route_str = ?', str)
}
Step 3, added Index action to my InterestPoint Controller :
def index
#interest_points = InterestPoint.by_location(params[:city]).by_category(params[:sub_category]).load
end
According to what I've got from you.
This controller should be InterestPoints Controller as its main
function is to display a list of all Interest points.
The interest point model should contain a method to
retrieve all interest points located in a given city and falling
under a given sub category and you just call it in the controller.
So you'll have something like this
class InterestPointsController < ApplicationController
def index
#interest_point = InterestPoint.located(params[:city]).categorized(params[:sub_category])
end
end
where in located and categorized are scopes in the interest point model

ActiveRecord association returns "NoMethodError for ActiveRecord::Relation"

I have 3 models with "1 to n" associations, like this
Client --1 to n--> Category --1 to n--> Item
In one page, I need to display a list of Items along with their Categories. This page is subject to 3 level of filtering:
Client filtering: I know the client id (I'll use 'id=2' in this example)
Category name: dynamic filter set by the user
Item name: dynamic filter set by the user
And I'm getting more and more confused with ActiveRecord Associations stuff
In my ItemsController#index, I tried this:
categories = Client.find(2).categories
.where('name LIKE ?', "%#{params[:filter_categories]}%")
#items = categories.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
The second line raises a NoMethodError undefined method 'items' for ActiveRecord::Relation. I understand that the first line returns a Relation object, but I cannot find a way to continue from here and get the list of Items linked to this list of Categories.
I also started to extract the list of categories ids returned by the first line, in order to use them in the where clause of the second line, but while writing the code I found it inelegant and thought there may be a better way to do it. Any help would be very appreciated. Thanks
models/client.rb
class Client < ActiveRecord::Base
has_many :categories
has_many :items, through: :categories
...
end
models/category.rb
class Category < ActiveRecord::Base
belongs_to :client
has_many :items
...
end
model/item.rb
class Item < ActiveRecord::Base
belongs_to :category
has_one :client, through: :category
...
end
You can only call .items on a category object, not on a collection. This would work:
#items = categories.first.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
To get what you want, you can do the following:
#items = Item
.where('category_id IN (?) AND name LIKE ?', categories, "%#{params[:filter_items]}%")
Assuming that in the end you are only interested in what is in #items, it would be even better to do it in one query instead of two, using joins:
#items = Item.joins(:category)
.where('items.name LIKE ? AND categories.name = ? AND categories.client_id = 2', "%#{params[:filter_items]}%", "%#{params[:filter_categories]}%")
You can try smth like this:
item_ids = Client.find(2).categories.inject([]) { |ids, cat| ids |= cat.item_ids; ids }
items = Item.find(item_ids)
This how you can get a list of nested objects that associated through another table.

Scoping a class method to current_user

I'm working on implementing a tagging system and I'm having problem querying for tagged objects with a scope.
For example, I would like to find all the user's items with a certain tag. With a class method I can currently find all the objects:
def self.tagged_with(name)
Tag.find_by_name(name).items
end
However, this has a problem. If I were to do something like: current_user.items.tagged_with(name) won't this existing method return ALL the items and not just items owned by the current_user? I suppose this is a simply querying issue but I can't figure out how to change a class method into something called on a collection. I have tried going the opposite way, to get a the collection through the tags, something like... tag.items.where(:user_id => current_user.id) but in this case, it's a many-to-many relationship and I haven't been able to get on thumb on this either.
What's the proper way to restrict a query like this?
Create an association on your User class that points to your Tag class.
class User < ActiveRecord::Base
has_many :tags
end
Then you can do:
current_user.tags.where(...)
If you don't already have an association in place, you'll need to create a migration to have the tags table reference your users table with a foreign key.
I think this will help you:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
So, basically you can define your method tagged_with directly into the association!
This example is took from the documentations ActiveRecord::Associations

Resources