In book/show I want to see it's sales in each existing library without abusing the views. Can the logic be somehow be transported into the model? Current book/show.haml:
= #book.name
- #libraries.each do |library|
= library.sales.where(book_id: #book.id).map(&:quantity).sum
My idea is to add a method in library.rb like:
def current_book_sold_by_library
#book = Book.find(:id)
#sales.where(book_id: #book.id).map(&:quantity).sum
sales.map(&:quantity).sum
end
But playing with this did not help. My setup:
book.rb:
class Book < ActiveRecord::Base
end
library.rb
class Library < ActiveRecord::Base
has_many :books, through: :sales
end
sale.rb
class Sale < ActiveRecord::Base
belongs_to :book
belongs_to :library
end
books_controller.rb
class BooksController < ApplicationController
def show
#libraries = Library.all
#sales = #book.sales
end
end
You may add a method with a book as a parameter to Library model:
# view
- #libraries.each do |library|
= library.book_sold_by_library(#book)
# Library model
def book_sold_by_library(book)
sales.where(book_id: book.id).map(&:quantity).sum
end
Related
In my Rails 6 app I have these models:
class User < ApplicationRecord
has_many :read_news_items
has_many :news_items, :through => :read_news_items
end
class NewsItem < ApplicationRecord
has_many :read_news_items
has_many :users, :through => :read_news_items
def read?(user)
read_news_items.where(:user_id => user.id).any?
end
end
class ReadNewsItem < ApplicationRecord
belongs_to :user
belongs_to :news_item
end
In my controller action I want to list all news items and highlight the ones that have not yet been read by the user:
class NewsItemsController < ApplicationController
def index
#news_items = NewsItem.all
end
end
The problem is that this generates N+1 queries for each record because the read?(current_user) gets called for each user record.
How can this problem be overcome?
I tried appending includes(:read_news_items) and joins(:read_news_items) to the database query in my controller but to no avail.
You could try:
class NewsItem < ApplicationRecord
has_many :read_news_items
def read?(user)
if read_news_items.loaded?
read_news_items.any? {|rni| rni.user_id == user.id }
else
read_news_items.where(:user_id => user.id).any?
end
end
end
class NewsItemsController < ApplicationController
def index
#news_items = NewsItem.includes(:read_news_items).all
end
end
OK, I learned something from every answer that was given here. So thanks for that.
I changed my read? method to the following which seems to have eliminated the N+1 queries:
class NewsItem < ApplicationRecord
def read?(user)
user.read_news_items.pluck(:news_item_id).include?(id)
end
end
I think it is more of a "Model Design" issue than a rails issue.
For clarity sake here is the business logic: I've Venues and I want to implement multiple APIs to get data about those venues. All this APIs have a lot in common, therefore I used STI.
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
end
# /app/models/api.rb
class Api < ApplicationRecord
belongs_to :venue
end
# /app/models/google_api.rb
class GoogleApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
# /app/models/other_api.rb
class OtherApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
That part works, now what I'm trying to add is Photos to the venue. I will be fetching those photos from the API and I realise that every API might be different. I thought about using STI for that as well and I will end up with something like that
# /app/models/api_photo.rb
class ApiPhoto < ApplicationRecord
belongs_to :api
end
# /app/models/google_api_photo.rb
class GoogleApiPhoto < ApiPhoto
def url
"www.google.com/#{reference}"
end
end
# /app/models/other_api_photo.rb
class OtherApiPhoto < ApiPhoto
def url
self[url] || nil
end
end
My goal being to have this at the end
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
has_many :photos :through => :apis
end
# /app/views/venues/show.html.erb
<%# ... %>
#venue.photos.each do |photo|
photo.url
end
<%# ... %>
And photo.url will give me the right formatting that is dependent of the api it is.
As I'm going deeper in the integration, something seems not right. If I had to Api the has_many :google_api_photo then every Api will have GoogleApiPhoto. What does not make sense to me.
Any idea how I should proceed from here?
I think I solved it.
By adding this to venue.rb
has_many :apis, :dependent => :destroy
has_many :photos, :through => :apis, :source => :api_photos
By calling venue.photos[0].url call the right Class based on the type field of the ApiPhoto
I've tried to save my model, but failed to save it.
#starship.rb
class Starship < ApplicationRecord
has_many :crew_members,inverse_of: :starship
accepts_nested_attributes_for :crew_members
has_many :holodeck_programs,inverse_of: :starship
accepts_nested_attributes_for :holodeck_programs
end
#crew_member.rb
class CrewMember < ApplicationRecord
belongs_to :starship
accepts_nested_attributes_for :starship
has_many :holodeck_programs,through: :starship
end
#holodeck_program.rb
class HolodeckProgram < ApplicationRecord
belongs_to :starship
belongs_to :crew_member
end
#controller
def create
#Starship,CrewMember and HolodeckProgram are new via CrewMember.new
#crew_member = CrewMember.new(crew_member_params)
#crew_member.save
.
.
end
.
.
private
def crew_member_params
params.require(:crew_member).permit(:name,:division,:starship_id,
starship_attributes: [:name,:id,
holodeck_programs_attributes: [:title,:starship_id,:crew_member_id]])
end
Because there is no crew_member_id in holodeck_programs_attributes, validation error happen.
I can not use inverse_of: :crew_member because of through in crew_member.rb
How can I handle it?
Actually you can create any number of crew_members, holodeck_programs while creating a starship.
But you are trying to create many starships, holodeck_programs for a single crew_members, look into
def crew_member_params
params.require(:crew_member).permit(:name,:division,:starship_id,
starship_attributes: [:name,:id,
holodeck_programs_attributes: [:title,:starship_id,:crew_member_id]])
end
So you need to change the create to,
def create
#starship = Starship.new(star_ship_params)
#starship.save
end
and crew_member_params to
def star_ship_params
params.require(:star_ship).permit(<attributes of star_ship>, holodeck_programs_attributes: [<attributes of holodeck_programs>], crew_members_attributes: [<attributes of crew_members>])
end
And also make sure that you have done all changes in the view accordingly
Please look into: NestedAttributes for more information regarding nested attributes and its usage.
I am building a simple budgeting app, and have a line of code that feels convoluted and overly complex. For context:
class User < ActiveRecord::Base
has_one :month_budget
has_many :expenditures, as: :spendable
end
class MonthBudget < ActiveRecord::Base
belongs_to :user
has_many :expenditures, as: spendable
end
class Expenditure < ActiveRecord::Base
belongs_to :spendable, polymorphic: true
end
Within my Expenditure class, I have defined a class method, add_expenditure:
class Expenditure < ActiveRecord::Base
def self.add_expenditure(user, params) #params passed will be in [:expenditure][*keys], in which possible keys are [:amount] or [:location]
if user.month_budget
user.month_budget.expenditures.create(params)
new_amount = user.month_budget.current_amount += params[:amount].to_d
user.month_budget.update(current_amount: new_amount)
end
end
end
Is there a more efficient way to add a value to the initial month_budget.current_amount column, and then record this new number to the database?
Cheers in advance!
Maybe you could try increment! method (http://apidock.com/rails/v4.2.1/ActiveRecord/Persistence/increment%21).
However, I am not sure if it works well with big decimals.
class Expenditure < ActiveRecord::Base
def self.add_expenditure(user, params)
if user.month_budget
user.month_budget.expenditures.create(params)
user.month_budget.increment!(:current_amount, params[:amount].to_d)
end
end
end
Using Rails. How to best rewrite the country_photo?
# country.rb
class Country < ActiveRecord::Base
has_many :zones
def country_photo
if !zones.blank? && !zones.first.shops.blank? && !zones.first.shops.first.photos.blank?
zones.first.shops.first.photos.first.url(:picture_preview)
end
end
end
# zones.rb
class Zone < ActiveRecord::Base
belongs_to :country
has_many :zone_shops
has_many :shops, :through => :zone_shops
end
# zone_shop.rb
class ZoneShop < ActiveRecord::Base
belongs_to :zone
belongs_to :shop
end
# shop.rb
class Shop < ActiveRecord::Base
end
Note that !x.blank? -> x.present?. Anyway, if you are ok with doing assignations in ifs (they are pretty common in Ruby), you can write:
def country_photo
if (zone = zones.first) &&
(shop = zone.shops.first) &&
(photo = shop.photos.first)
photo.url(:picture_preview)
end
end
If you like fancy abstractions, with Ick you can write:
def country_photo
zones.first.maybe { |zone| zone.shops.first.photos.first.url(:picture_preview) }
end
Assuming you want to show an image in a view, I would do something like this:
# show.html.haml
- if #country.photo
image_tag #country.photo.url(:picture_preview)
# country.rb
class Country < ActiveRecord::Base
def photo
zones.first.photo unless zones.blank?
end
end
# zone.rb
class Zone < ActiveRecord::Base
def photo
shops.first.photo unless shops.blank?
end
end
# shop.rb
class Shop < ActiveRecord::Base
def photo
photos.first unless photos.blank?
end
end