My app is based on the subdomain. Each manager has it's own subdomain that should show only his auctions. I have:
class Lot < ActiveRecord::Base
belongs_to :auction
end
class Auction < ActiveRecord::Base
has_many :lots
belongs_to :manager
end
class manager < ActiveRecord::Base
has_many :auctions
end
If the app was accessed using a subdomain, I have a before_filter that do the following:
def load_manager
#loaded_manager = Manager.find_by_subdomain(request.subdomain)
end
and on my Lot's default_scope, i'd like to do the following:
default_scope { #loaded_manager.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", #loaded_manager.id) : where(deleted: false) }
So that way wherever I'm on the website I'll just show the lots that belongs to the manager's auctions.
The problem is that I can't access #loaded_manager on the model. What's the best way for doing this?
http://railscasts.com/episodes/388-multitenancy-with-scopes?view=comments solves this issue!
You may store current manager in Manager model in before filter:
def load_manager
Manager.set_current(request.subdomain)
end
class Manager < ActiveRecord::Base
cattr_accessor :current
def self.set_current(subdomain)
self.current = self.find_by_subdomain(subdomain)
end
end
class Lot < ActiveRecord::Base
default_scope { Manager.current.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", Manager.current.id) : where(deleted: false) }
end
Update
As #Mik_Die noticed it is not thread safe, for thread safe solution a reader may look at railscasts - multitenancy-with-scopes (code here). There we just store current_id in Thread.
Models don't have access to controller's instance variables (and should not have - it breaks MVC principles)
Probably, using default scope in this case is bad idea. Consider using usual scope like this
scope :for_manager, lambda{ |manager| manager.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", manager.id) : where(deleted: false) }
And then in your controller
#lots = Lot.for_manager(#loaded_manager)
Related
Have an app where current_user can be a Client or and Admin. A Client can have many Accounts and an AccountStatement belongs to both the Account and the Client.
However there are some auto generated statements that we don't want to show the clients. I'd like to add a model scope of some kind that would look something like
def account_statements
if current_user.is_a?(Client)
super.where(auto_generated: false)
else
super
end
end
so if I ran .account_statements on a valid instance of Account or Client it would only return a subset of the statements if the current_user is a Client, but all of them if the current_user is an Admin. Is there any way to do this?
Just give each class its own method:
class Admin < ApplicationRecord
...
def account_statements
AccountStatement.all
end
end
class Client < ApplicationRecord
...
def account_statements
AccountStatement.where(auto_generated: false)
end
end
If you need Client to have a defined relationship to AccountStatement, things get a little more tricky:
class Client < ApplicationRecord
has_many :account_statements
# this method will supersede the relationship
def account_statements
...
end
end
In which case, you can either:
use a different name for the method
use this cool relationship-with-scope trick to do something like this:
class Client < ApplicationRecord
has_many :account_statements, -> { without_auto_generated }
end
class AccountStatement < ApplicationRecord
belongs_to :client
belongs_to :account
scope :without_auto_generated, -> { where(auto_generated: false) }
end
I have an activerecord class method scope that returns all when the scope should remain unchanged. However I would expect it to use the counter cache when chaining size to the all scope. Here is an example:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(only_approved:)
if only_approved
where(approved: true)
else
all
end
end
end
# This does not use the counter cache but should since the scope is unchanged
Post.first.comments.filtered(only_approved: false).size
So it looks like Post.comments.size triggers the counter cache while Post.comments.all.size does not. Is there a way around this?
This happens because of how the counter_cache works. It needs 2 things:
Add the counter_cache: true to the belonging model (Comment)
Add a column comments_count to the having model (Post)
The column added to the Post model gets updated everytime you create or destroy a model so it will count all existing records on the table. This is the reason why it won't work on a scope (a scope might be useful to filter the resulting records, but the actual column comments_count is still counting the whole table).
As a workaround I'd suggest you to take a look at and see if it can be used for your usecase https://github.com/magnusvk/counter_culture.
From their own repo:
class Product < ActiveRecord::Base
belongs_to :category
scope :awesomes, ->{ where "products.product_type = ?", 'awesome' }
scope :suckys, ->{ where "products.product_type = ?", 'sucky' }
counter_culture :category,
column_name: proc {|model| "#{model.product_type}_count" },
column_names: -> { {
Product.awesomes => :awesome_count,
Product.suckys => :sucky_count
} }
end
The only way I found to deal with this is to pass the scope to the class method and return it if no additional scope is to be added. It's not as clean but it works. Here is the updated code:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
def self.filtered(scope:, only_approved:)
if only_approved
scope.where(approved: true)
else
scope
end
end
end
# This works with counter cache if the scope is returned as is
Comment.filtered(scope: Post.first.comments, only_approved: false).size
I have two sub models, called: Service and Product that inherits from ProductBase. And I have another model to consume it. Acquire that have many AcquireBasket. Check out my code:
product_base.rb:
class ProductBase < ActiveRecord::Base
extend ::EnumerateIt
include Searchable
self.table_name = 'products'
end
product.rb:
class Product < ProductBase
default_scope { where(kind: ProductKind::PRODUCT) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::PRODUCT
self.status = ProductStatus::DRAFT
end
end
service.rb:
class Service < ProductBase
default_scope { where(kind: ProductKind::SERVICE) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::SERVICE
self.status = ProductStatus::DRAFT
end
end
acquire_basket.rb:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product
end
In some part of my project, I get a list (acquire baskets) of both models, Service and Product. And I need to check if I have services inside of it.
My code to check was:
def services_in?(acquire)
acquire.baskets.map(&:product).detect(&:service?)
end
The code works, ONLY if I pass services first, and products after!! Or if I have only one of them.
You should be able to utilize the descendents method to iterate over all of the subclasses
I can't find the answer in blog post around the world, so I will share with you:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product, class_name: 'ProductBase'
end
The problem was, when I try to find (lazily) in a ActiveRecord::Relation, Rails lookup (I think) to just Product model. And It can't find other type models inside of it. So using this typo I put it to work.
I'm trying to query across models with the following setup
Class Scorecard < AR::Base
default_scope where(:archived => false)
belongs_to :user
has_many :scorecard_metrics
end
Class ScorecardMetric < AR::Base
belongs_to :scorecard
end
Class User < AR::Base
has_many :scorecards
end
I am trying to query from scorecard metrics with a named scope that joins scorecard and I want it to include the default scope for scorecard, my current implementation (which works) looks like this
# on ScorecardMetric
scope :for_user, lambda {
|user| joins(:scorecard).
where("scorecards.user_id = ? and scorecards.archived = ?", user.id, false)
}
This just seems messy to me, is there any way to join and include the default scope of the joined association?
Looks like I found the answer I was looking for, I just did this
scope :for_user, lambda { |user| joins(:scorecard).where('scorecards.user_id = ?', user.id) & Scorecard.scoped }
which is much nicer without the duplicated logic
I'd like to extract out logic from the controllers to somewhere that it can be more DRY. What's the best way of handling something like the following in Rails?
For instance, as opposed to having the query in the controllers, I could place it in the model:
class SmoothieBlender < ActiveRecord::Base
belongs_to :user
def self.get_blenders_for_user(user)
self.where(["user_id = ?", user.id])
end
end
Or would it be better to create a module as a service layer and include that in each model that uses it?
module BlenderUser
def get_blenders_for_user(user)
SmoothieBlender.where(["user_id = ?", user.id])
end
end
class SmoothieBlender < ActiveRecord::Base
include BlenderUser
belongs_to :user
end
class User < ActiveRecord::Base
include BlenderUser
has_many :smoothie_blenders
end
Or just make it a full blown service class that's accessible from the User and Blender controller? Where would you put this class?
class BlenderService
def self.get_blenders_for_user(user)
SmoothieBlender.where(["user_id = ?", user.id])
end
end
I'm new to Ruby and Rails, so if this is a silly question/syntax is incorrect, forgive me. Thanks in advance!
I'd create a named_scope (I think it's just scope in Rails 3)
class SmoothieBlender < ActiveRecord::Base
belongs_to :user
scope :for_user, lambda { |user_id|
where("user_id = ?", user_id)
}
end
This way you can call
SmoothieBlender.for_user(user.id)