I have two models:
class country < ActiveRecord::Base
has_many :companies
end
class company < ActiveRecord::Base
belongs_to :country
end
In my view index for company I can show which country the company belongs to by showing the following:
<%= company.country_id %>
This will show me the id number it's associated with, but I can't seem to work out how to resolve this back to the country name which is country.name, everything I seem to try crashes rails, I don't think I'm approaching the problem the correct way?
<%= company.country.try(:name) %>
really ought to do what you want.
Edited as comment suggested.
I wouldn't recommend using #try though. It is much better when you tell Rails to give you the filtered collection, that's what controllers are for.
In this case you'd have
class CompaniesController < ApplicationController
def index
#companies = Company.where('country_id IS NOT NULL') #if using the relational db
# or #companies = Company.all.select { |company| company.country.present? }
end
end
and in your view
<% #companies.each do |company| %>
<%= company.country.name %>
<% end %>
Update:
Even better aproach is to create a named scope in your model for such things.
class Company < ActiveRecord::Base
#...
scope :with_country, ->() { where('country_id IS NOT NULL') }
end
Now you can change your controller action
def index
#companies = Company.with_country
end
This will make your code much more consistent and readable.
Related
I'm building e-commerce application in which you can set user specific prices for products. If price for specific product for specific user is not set, it will show default product price.
It works fine, but I'm looking for more efficient solution since I'm not happy with current one.
Tables:
users
products (all product info + regular_price)
prices (user_id, product_id, user_price)
Models:
class User < ApplicationRecord
has_many :prices
end
class Product < ApplicationRecord
has_many :prices
validates :name, presence: true
def self.with_user_prices(current_user)
Product.joins(
Product.sanitize_sql_array(['LEFT OUTER JOIN prices ON prices.user_id = ?
AND products.id = prices.product_id', current_user])
).select('products.*, prices.user_price')
end
end
class Price < ApplicationRecord
belongs_to :product
belongs_to :user
end
How I get all products with user specific prices in controller:
#products = Product.with_user_prices(current_user)
How I display them in view:
<% #products.each do |product| %>
<%= product.user_price ? product.user_price : product.regular_price %>
<% end %>
As you see I'm currently joining prices table and then in view I display user_price (prices table) if it exists, otherwise regular_price (products table).
I would love to solve everything in a single query keeping only one price column with appropriate value according to the current_user
You can make use of SQL COALESCE function:
class Product < ApplicationRecord
# ...
def self.with_user_prices(user)
includes(:prices).where(prices: { user_id: user.id }).select(
'products.*, COALESCE(prices.user_price, products.regular_price) as price'
)
end
end
Then you can use it simply by:
<%= product.price %>
Note that I simplified Product.with_user_prices method a little bit by using includes, which is gonna generate SQL LEFT JOIN query since there's condition on prices.
New Answer:
please do not mark this answer as correct because I just basically extended Marek's code and yours into mine, as after several attempts I came to what you've already done anyway, but I'm just putting it here in case it helps anyone:
app/models/product.rb
class Product < ApplicationRecord
def self.with_user_prices(user)
joins(
sanitize_sql_array([
"LEFT OUTER JOIN prices on prices.product_id = products.id AND prices.user_id = ?", user.id
])
).select(
'products.*',
'COALESCE(prices.user_price, products.regular_price) AS price_for_user'
)
end
end
controller:
#products = Product.with_user_prices(current_user)
view:
<% #products.each do |product| %>
<%= product.price_for_user %>
<% end %>
Old Answer (Inefficient Code):
Untested but can you try the following? (Not sure if this is more or less efficient than your approach)
app/models/product.rb
class Product < ApplicationRecord
has_many :prices
def price_for_user(user)
prices.includes(:user).where(
users: { id: user.id }
).first&.user_price || regular_price
end
end
controller:
# will perform LEFT OUTER JOIN (to eager load both `prices` and `prices -> user`) preventing N+1 queries
#products = Product.eager_load(prices: :user)
view:
<% #products.each do |product| %>
<%= product.price_for_user(current_user) %>
<% end %>
I have the following two models:
class Company < ActiveRecord::Base
has_many :employees # A company can have 1000's of employees.
end
class Employee < ActiveRecord::Base
belongs_to :company
end
One of my use cases looks like this
In the controller:
def my_action
#companies = Company.where(country: 'GB').preload(:employees)
#title = params[:title] # E.g. 'CTO'
end
In the view:
- #companies.each do |c|
= c.employees.where(title: #title).first.last_name
The problem with the above is that it will create an N+1 in the view
Now I could change the view to:
# In the view
- #companies.each do |c|
= c.employees.select{|e| e.title == #title}.first.last_name
This will solve the N+1 but it will still load out potentially 1000's of employee records even though I don't need them (I only need the ones with the right title).
I could solve that by:
class Company < ActiveRecord::Base
has_many :employees
has_many :ctos, ->{ where(title: 'CTO') }
end
# In the controller
def my_action
#companies = Company.where(country: 'GB').preload(:ctos)
end
# In the view
- #companies.each do |c|
= c.ctos.first.last_name
The problem with this however is that it would require me to add associations for every single possible type on Company.
So the question is, how can I solve this?
companies = Company.where(country: 'GB')
title = params[:title]
#company_employee = Employee.where(company: companies, title: title).group_by(&:company)
#company_employee will be hash with structure { company => [employees] }
I am fairly new to RoR and trying to get a basic app to work - I have a 'books' model and a 'genre' model. I wish to create a page that randomly generates books of different genre's for a user to select.
I have created a 'random_book' controller, but am unsure on how to proceed with the random selection and display.
Any help/pointers would be appreciated.
Edit:
Here's the work I've been doing in the random_book model:
" load 'user.rb'
class random_book < ActiveRecord::Base
belongs_to :book
belongs_to :genre
def get_random_book
find(:all).sample(5)
end
"
Thank you.
Based on discussion
4 models
Book
Genre
UserBook
User
They will look something like this:
class User < ActiveRecord::Base
has_many :user_books
has_many :books, through: :user_books
end
class Genre < ActiveRecord::Base
has_many :books
def fetch_random_books(qty)
#you want to make sure you don't error out by requesting a sample of an empty list of books so check first. The qty argument lets you control the number of books for the search
unless self.books.empty?
self.books.limit(qty).sample
end
end
end
class Book < ActiveRecord::Base
belongs_to :genre
has_many :user_books
end
class UserBook < ActiveRecord::Base
belongs_to :book
end
I would most likely use a different route for the random book section, because it's not very url-friendly to say code-descriptive things like user_book.
There are 4 things to do
Create a new route to get a list of genre_ids that a user chooses.
Create an action that correlates to the route you created that renders a list of boos and adds those books to a users list
Create a form in a view (any view, like a sidebar in an existing books view, doesn't matter) this form will post the route and action you just made
Create a view to render the book list (the easy / DRY way is to add a few elements to the existing books index to let users know its a random generated list of books based on their genre pics)
Add the route
post "/random-books", to: "books#random_books", as: :random_books
Create the action in the books_controler
def random_books
if params[:genre_ids]
genres = Genre.where(id: params[:genre_ids])
#books = []
genres.each do |genre|
#books << genre.fetch_random_books(10)
end
else
#books = nil
end
respond_to do |format|
format.html { render action: :index }
end
end
Then create a form that makes a post request to the index action of the books_controller -- You can parse the form and update the UserBook model inside that action, and then display list of books all at the same time.
<%= form_tag(random_books_path, method: :post) do %>
<ul>
<% Genre.all.each do |genre| %>
<li>
<label class='genre-select'>
<%= check_box_tag 'genre_ids[]', genre.id -%>
<%= genre.name %>
</label>
</li>
<% end %>
</ul>
<%= submit_tag "Fetch Book List"%>
<% end %>
-- The last one I'm sure you can do, it returns a books object list so parse it however works best for you. In the controller action you can automatically add the ids for the books to the UserBook model by adding this inside the controller action:
#books.each{ |book| book.user_books.create(user: user)}
Basically I have a Shop, Category and a join model ShopCategory with additional attributes
class Shop
has_many :shop_categories
has_many :categories, through: :shop_categories
class Category
has_many :shop_categories
has_many :shops, through: :shop_categories
class ShopCategory
belongs_to :shop
belongs_to :category
I have a shop form which I'd like to create or update the shop through it.
My first thought is to create a virtual attribute called :categories and to have the model handle the setter and getter through it, something like this (pseudocode for simplicity):
def categories=(cats)
cats.each do |c|
check if a ShopCategory exists with this shop (self) and that category.
if doesn't exist, create one, if exists ignore
for all the categories in self that weren't touched, delete that ShopCategory
end
end
but I feel this would cause problems in the long run because of the connection of 3 models and not though a controller
However, I can't seem to think of a simple way to have a create and update methods in the shops_controller for handling this
def update
#shop = Shop.find params[:id]
cats = params[:shop].delete :categories
#shop.update_attributes(shop_params)
## should I have a category update method here? How would I handle errors? This gets complicated
end
It sounds like you want a nested model form, for editing both a Shop and its associated ShopCategories.
Basically, what it entails is on the form for your Shop, you can simply iterate over the associated ShopCategories and print out fields for them, to edit them all together. Rails will automatically handle it all, as long as the parameters are structured correctly.
https://github.com/nathanvda/cocoon is a gem for making nested model forms easier.
There is also a tutorial on Railscasts:
http://railscasts.com/episodes/196-nested-model-form-revised
Collections
I don't know how experienced you are with Ruby on Rails, but you may wish to look at some of the documentation pertaining to collections
What you're looking at is how to populate your collections - which is actually relatively simple:
#app/controllers/shops_controller.rb
Class ShopsController < ApplicationController
def create
#shop = Shop.new(shop_params)
#shop.save
end
private
def shop_params
params.require(:shop).permit(:your, :attributes, category_ids: [])
end
end
This will allow you to use the following form:
#app/views/shops/new.html.erb
<%= form_for #shop do |f| %>
<% Category.all.each do |category| %>
<%= f.check_box :category_ids, category.id %>
<% end %>
<% end %>
--
Modularity
In terms of validating your collections for uniqueness, you will be best using DB, or Association-level validation:
class Shop
has_many :categories, -> { uniq }, through: :shop_categories
This will essentially create only unique categories for your shop, which you can populate with the method described above.
I am trying to develop ratings for my application, where a User is able to set a specific rating for a comment. I have followed the following tutorial in order to do so.
Here are my associations:
class Rating < ActiveRecord::Base
belongs_to :comment
belongs_to :user
end
class Comment < ActiveRecord::Base
has_many :ratings
belongs_to :user
end
class User < ActiveRecord::Base
has_many :ratings
has_many :comments
end
My problem here is that, in the index action of my comments controller, I need to include the rating that the user has done for that comment. In the tutorial is just shown how to select a particular rating by doing this:
#rating = Rating.where(comment_id: #comment.id, user_id: #current_user.id).first
unless #rating
#rating = Rating.create(comment_id: #comment.id, user_id: #current_user.id, score: 0)
end
However, I will have several ratings, because in my controller I have:
def index
#comments = #page.comments #Here each comment should have the associated rating for the current_user, or a newly created rating if it does not exist.
end
You want to find the comment's rating where the rating's user_id matches the current user.
<%= comment.ratings.where(user_id: current_user.id).first %>
However this sort of logic is pretty cumbersome in the views, a better strategy would be to define a scope in Rating that returns all ratings made by a specific user.
class Rating
scope :by_user, lambda { |user| where(user_id: user.id) }
end
class Comment
# this will return either the rating created by the given user, or nil
def rating_by_user(user)
ratings.by_user(user).first
end
end
Now in your view, you have access to the rating for the comment created by the current user:
<% #comments.each do |comment| %>
<%= comment.rating_by_user(current_user) %>
<% end %>
If you want to eager load all ratings in your index page, you can do the following:
def index
#comments = page.comments.includes(:ratings)
end
You can then find the correct rating with the following:
<% #comments.each do |comment| %>
<%= comment.ratings.find { |r| r.user_id == current_user.id } %>
<% end %>
This would return the correct rating without generating any extra SQL queries, at the expense of loading every associated rating for each comment.
I'm not aware of a way in ActiveRecord to eager load a subset of a has_many relationship. See this related StackOverflow question, as well as this blog post that contains more information about eager loading.