Rails 3 default scope, scope with override - ruby-on-rails

I have a situation where the behavior of an existing app is changing and it's causing me a major headache.
My app has Photos. Photos have a status: "batch", "queue", or "complete". All the existing Photos in the app are "complete".
99% of the time I need to only show complete photos, and in all of the existing codebase I need every call to Photos to be limited to only complete photos.
However, in the screens related to uploading and classifying photos I need to be able to fairly easily override that default scope to show batched or queued photos.
Like many others, I need to find a way to easily override the default scope in certain situations. I looked at these questions (1, 2) and they don't seem to answer what I'm looking for.
The code I wish worked is this:
class Photo < ActiveRecord::Base
...
default_scope where(:status=>'complete')
scope :batch, unscoped.where(:status=>'batch')
scope :queue, unscoped.where(:status=>'queue')
...
end
However, that doesn't work. I tried wrapping the scope methods in lambdas, and that didn't work either.
I realize default_scope comes with baggage, but if I can't use it with overrides then I'm looking at adding scope :complete ... and having to comb through every call to photos in my existing app and add .complete in order to filter unprocessed photos.
How would you solve this problem?

def self.batch
Photo.unscoped.where(:status=>"batch")
end
This thread is more authoritative: Overriding a Rails default_scope

I give it a shot. Lets say you want to remove a where clause from a default scope (and not just override it with another value) and keep associations you can try this:
class Photo < ActiveRecord::Base
default_scope where(:status => 'complete').where(:deleted_at => '').order('id desc')
def self.without_default_status
# Get the ActiveRecord::Relation with the default_scope applied.
photos = scoped.with_default_scope
# Find the where clause that matches the where clause we want to remove
# from the default scope and delete it.
photos.where_values.delete_if { |query| query.to_sql == "\"photos\".\"status\" = 'complete'" }
photos
end
end

Related

Filter ActiveRecord objects based on model method?

Is there a way to use a filter criterion in where, which is not a DB column. If I have a Movie model with the following method:
def blockbuster?
imdb_rating > 8
end
is there a way to do something like Movie.where(:blockbuster? => true). I know that in this particular example it's possible to just use the imdb_rating attribute (Movie.where('imdb_rating > ?', 8)), but there are cases, when a method does a more complex logic. Currently, if I want to do this, I must call Movie.all.select(&:blockbuster?), but I want to do this at the DB level. Thank you.
P.S. I am sure that similar questions are asked many times, but I can't seem to think of the right keywords to find them here or on Google. Please, post a link if this is answered elsewhere.
Have you tried making it into a scope? There is some information on scopes in the Rails documentation.
Basically, with your method, you'd do something like:
class Movie < ActiveRecord::Base
scope :blockbuster, -> { where('imdb_rating > ?', 8) }
end
Movie.blockbuster # returns all relevant objects as an ActiveRecord relation
Class methods are also available as scopes:
class Movie < ActiveRecord::Base
def self.blockbuster?
where('imdb_rating ?', 8)
end
end
Movie.blockbuster? # also returns all relevant objects as an ActiveRecord relation

different default_scopes for searching and saving in rails (or a better implementation than the one I have)?

I have a rails app with 2 types of users, authenticated and unauthenticated (separated by a email_authenticated:boolean in the database). when I create a user I want it to be unauthenticated but every time I perform any function I want to perform that upon the authenticated list by default. I initially tried to do this by providing a default_scope but found out that this modifies the way the record is saved overrides the default (e.g. the default turns to true rather than false in the example)
# email_authenticated :boolean default(FALSE), not null
class User < ActiveRecord::Base
default_scope { where(email_authenticated: true) }
scope :authenticated, ->{ where(email_authenticated: true) }
scope :unauthenticated, ->{unscoped.where(email_authenticated: false)}
end
does anyone have a suggestion for a way to have a scope only apply on searches, or a smarter way of achieving what I'm going for. I don't want to have to call User.authenticated every time I search if I remove the default scope, similarly I don't want to call User.unauthenticated every time I save on the other hand.
Kind of seems like a hack, but you can do:
class User < ActiveRecord::Base
default_scope { where("email_authenticated = ?", true) }
end
Documented here: http://apidock.com/rails/ActiveRecord/Base/default_scope/class. I just tested it and it works, without the side effect on create.

Scope for multiple models

I have several objects that all have an approved field.
What would be the best way to implement a scope to use across all models?
For example, I have a sighting object and a comment object. They both have to be approved by an admin before being availalbe to the public.
So how can I create a scope that returns comment.approved as well as sighting.approved respectively without repeating it on each model? Is this where concerns comes into play?
While just declaring a scope in each model is fine if you just want the scoping functionality. Using an ActiveSupport::Concern will give you the ability to add additional methods as well if that's something you think is going to happen. Here's an example:
# /app/models/concerns/approved.rb
module Approved
extend ActiveSupport::Concern
included do
default_scope { where(approved: false) }
scope :approved, -> { where(approved: true) }
end
def unapprove
update_attribute :approved, false
end
end
class Sighting < ActiveRecord::Base
include Approved
end
class Comment < ActiveRecord::Base
include Approved
end
Then you can make calls like Sighting.approved and Comment.approved to get the appropriate list of approved records. You also get the unapprove method and can do something like Comment.approved.first.unapprove.
In this example, I've also included default_scope which will mean that calls like Sighting.all or Comment.all will return only unapproved items. I included this just as an example, it may not be applicable to your implementation.
Although I've noticed the scope pulled from the concerns needs to be the last scope when concatenating scopes. I'm not quite sure why.
Comment.existing.approved
When I tried it as:
Comment.approved.existing
It failed silently.
And I take that back. I was migrating older code and using scopes with conditions instead of lambdas. When I replaced :conditions the scope order no longer mattered.
scope :existing, -> { where("real = 1") }
replaced
scope :existing, :conditions => "real = 1"

Override mongomapper has_many association

I use Rails 3 with MongoMapper.
I want to add some records to the result of has many association.
For example, user has_many posts
class User
include MongoMapper::Document
many :posts
end
By default it will show only posts which belongs to the user, but if he/she specify special option in query (or in the user's settings menu, say show-commented=true), then I also need to add posts where user left any comments. So I think to override posts method
def posts
super + (show_commented_posts ? commented_posts : [])
end
But of course it doesn't work. How can I correctly override this method using mongo_mapper? Or is there any better approach for that problem?
Overriding methods on mongomapper is a very bad idea, you should try to refrain from doing it as it creates a lot of problems that are hard to trace back (I've been burned before by this).
Instead, you should consider using a scope such as
class Post
scope :related_to_user, lambda {|user| where('$or' => [ {user_id: user.id}, {'comments.user_id' => user.id}]) }
end
Then you can call
Post.related_to_user(current_user)

Referring to instance in has_many (Rails)

I have a Game model which has_many :texts. The problem is that I have to order the texts differently depending on which game they belong to (yes, ugly, but it's legacy data). I created a Text.in_game_order_query(game) method, which returns the appropriate ordering.
My favourite solution would have been to place a default scope in the Text model, but that would require knowing which game they're part of. I also don't want to create separate classes for the texts for each game - there are many games, with more coming up, and all the newer ones will use the same ordering. So I had another idea: ordering texts in the has_many, when I do know which game they're part of:
has_many :texts, :order => Text.in_game_order_query(self)
However, self is the class here, so that doesn't work.
Is there really no other solution except calling #game.texts.in_game_order(#game) every single time??
I had a very similar problem recently and I was convinced that it wasn't possible in Rails but that I learned something very interesting.
You can declare a parameter for a scope and then not pass it in and it will pass in the parent object by default!
So, you can just do:
class Game < ActiveRecord
has_many :texts, -> (game) { Text.in_game_order_query(game) }
Believe or not, you don't have to pass in the game. Rails will do it magically for you. You can simply do:
game.texts
There is one caveat, though. This will not work presently in Rails if you have preloading enabled. If you do, you may get this warning:
DEPRECATION WARNING: The association scope 'texts' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
Following up using PradeepKumar's idea, I found the following solution to work
Assuming a class Block which has an attribute block_type, and a container class (say Page), you could have something like this:
class Page
...
has_many :blocks do
def ordered_by_type
# self is the array of blocks
self.sort_by(&:block_type)
end
end
...
end
Then when you call
page.blocks.ordered_by_type
you get what you want - defined by a Proc.
Obviously, the Proc could be much more complex and is not working in the SQL call but after there result set has been compiled.
UPDATE:
I re-read this post and my answer after a bunch of time, and I wonder if you could do something as simple as another method which you basically suggested yourself in the post.
What if you added a method to Game called ordered_texts
def ordered_texts
texts.in_game_order(self)
end
Does that solve the issue? Or does this method need to be chainable with other Game relation methods?
Would an Association extension be a possibility?
It seems that you could make this work:
module Legacy
def legacy_game_order
order(proxy_association.owner.custom_texts_order)
end
end
class Game << ActiveRecord::Base
includes Legacy
has_many :texts, :extend => Legacy
def custom_texts_order
# your custom query logic goes here
end
end
That way, given a game instance, you should be able to access instance's custom query without having to pass in self:
g = Game.find(123)
g.texts.legacy_game_order
Here is a way where you can do it,
has_many :texts, :order => lambda { Text.in_game_order_query(self) }
This is another way which I usually wont recommend(but will work),
has_many :texts do
def game_order(game)
find(:all, :order => Text.in_game_order_query(game))
end
end
and you can call them by,
game.texts.game_order(game)
Im not sure what your order/query looks like in the in_game_order_query class method but i believe you can do this
has_many :texts, :finder_sql => proc{Text.in_game_order_query(self)}
Just letting you know that I have never used this before but I would appreciate it if you let me know if this works for you or not.
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more documentation on :finder_sql
I think if you want runtime information processed you should get this done with:
has_many :texts, :order => proc{ {Text.in_game_order_query(self)} }

Resources