using if with scope on model - ruby-on-rails

I am trying to make a named scope called :current_season where the it will correctly identify the records associated with the year we are in. Mostly easy enough except I want everything June and later to use the current year and everything prior to June to use the previous year.
in rails 3.1 I can easily use:
scope :current_season, lambda { where('season = ?',Time.now.year) } if Time.now.month >= 6
to get the scope to only work if we are at the end of the year and :
scope :current_season, lambda { where('season = ?',Time.now.year - 1) } if Time.now.month < 6
But it seems to wasteful to have to name it all twice and not use an if/else type of thing or be able to call in something I define below to show the exact year such as:
scope :current_season, lambda { where('season = ?',:current_season_year) }
def current_season_year
if Time.now.month >= 6
Time.now.year
else
Time.now.year - 1
end
end
But that just laughs at me when I try it. Is there a cleaner way? I will also have a scope :last_season and scope :previous_season most likely and they will follow similar logic.
thanks in advance for any advice!

Named scopes are just a DSL for writing a class methods that all have a similar functionality. Whenever you find them to be limiting you, just switch to a class method instead:
def self.current_season
year = Time.now.month >= 6 ? Time.now.year : Time.now.year - 1
where('season = ?', year)
end
Of course, you could also include that in a scope like this:
scope :current_season, do
# same code as above...
end
It's just going to define it as a class method on the model though. The tradeoff is clarity in the intention of a scope (it's expected to return a chainable ActiveRecord::Relation) versus clarity in documentation (if you run something like RDoc it isn't going to notice a method available at Model.current_season because it hasn't been defined in the code yet).
Update:
There is one additional benefit from using a scope instead of a class method:
User.admin.create name: 'Corey' #=> <User: #name="Corey" #admin=true>
You can use a scope to create an object with certain parameters, as well. In this case, this isn't very useful, but it's worth considering when deciding which to use.

Related

Custom ActiveAdmin Filter for Date Range - Complex Logic in Method

I am trying to create a custom ActiveAdmin filter that takes date_range as a parameter. Every solution I've found has been for excessively simple model methods.
Is there a way to pass both parameters into the model ransacker method, and/or at the very least to control the order in which these parameters are passed as well as to know which one is being passed? (end_date vs. start_date -- start_date is passed first, whereas I might be able to work around this is end_date were sent first). Any alternative solution, which would not break all other filters in the application (ie, overwriting activeadmin filters to use scopes - this is one filter out of hundreds in the application) welcome as well.
Thank you!
admin/model.rb
filter :model_method_in, as: :date_range
models/model.rb
ransacker :model_method, :formatter => proc { |start_date, end_date|
Model.complicated_method(start_date, end_date)
} do |parent|
parent.table[:id]
end
...
def method_for_base_queries(end_date)
Model.long_complicated_sql_call_using_end_date
end
def complicated_method(start_date, end_date)
model_instances = method_for_base_queries(end_date)
model_instances.logic_too_complex_for_sql_using_start_date
end
Similar question, but filter/model logic was simple enough for an alternative solution that didn't require both parameters to be passed in: Custom ActiveAdmin filter for Date Range
This might help. Given your index filter
filter :model_method, as: :date_range
you can write the following in your model:
scope :model_method_gteq_datetime, -> (start_date) {
self.where('users.your_date_column >= ?', start_date)
}
scope :model_method_lteq_datetime, -> (end_date) {
# added one day since apparently the '=' is not being counted in the query,
# otherwise it will return 0 results for a query on the same day (as "greater than")
self.where('users.your_date_column <= ?', (Time.parse(end_date) + 1.day).to_date.to_s)
}
def self.ransackable_scopes(auth_object = nil)
[model_method_gteq_datetime, :model_method_lteq_datetime]
end
..._gteq_datetime and ..._lteq_datetime is how Activeadmin interprets the two dates in a custom date_range index filter (see also the corresponding url generated after adding the filter).
I've written a sample query that fits my case (given that users is a model related to the current one), since I don't know the complexity of yours.
I'm using:
Ruby 2.3.1
Rails 5.0.7
Activeadmin 1.3.0

Pagination Best Practices Ruby

So I am developing a rails app, and I am working on paginating the feed. While I was doing it I wondered if I was doing it the right way because my load times were over 1500ms. My code was:
stories = Story.feed
#stories = Kaminari.paginate_array(stories).page(params[:page]).per(params[:pageSize])
I have a few questions about this:
Should I be paginating Story.feed, or is there some sort of method
that only returns some the stories I need?
Is this load time normal?
What are other things I can be doing to optimize this
(Also, Story.feed returns an array of story objects. The code for that is here:
def self.feed
rawStories = Story.includes([:likes, :viewers, :user, :storyblocks]).all
newFeaturedStories = rawStories.where(:featured => true).where(:updated_at.gte => (Date.today - 3)).desc(:created_at).entries
normalStories = rawStories.not_in(:featured => true, :or => [:updated_at.gte => (Date.today - 3)]).desc(:created_at).entries
newFeaturedStories.entries.concat(normalStories.entries)
end
I am using mongoid and mongodb
The issue is that you get all feeds from db in an array and this takes long time.
I suggest you use the any_of query from this great gem.
From there, do:
def self.feed_stories
newFeaturedStories = Story.where(:featured => true).where(:updated_at.gte => (Date.today - 3.days))
normalStories = Story.not_in(:featured => true, :or => [:updated_at.gte => (Date.today - 3.days)])
Story.includes([:likes, :viewers, :user, :storyblocks]).any_of(newFeaturedStories, normalStories).desc(:created_at)
end
Then paginate this:
selected_stories = Story.feed_stories.per(page_size).page(page)
Dont really understand what are your entries but get them at this moment.
To sum up: the idea s to make a unique paginated db query.
I suspect that when you call Kaminari.paginate_array on an ActiveRecord::Relation, it causes the whole result set to be fetched from DB and loaded in memory similar to calling Model.all.to_a.
To avoid this, I'd first find a way to turn Story.feed into a scope, rather than a class method. Superficially they'll seem the sameā€”the differences are subtle but deep. See Active Record scopes vs class methods.
Next, ditch paginate_array in favor of chain Kaminari's page() and per() scopes.
For example (simplified version of yours):
class Article < ActiveRecord::Base
scope :featured, -> { where(featured: true) }
scope :last_3_days, -> { where(:updated_at.gte => (Date.today - 3)).desc(:created_at) }
scope :feed, -> { featured.last_3_days }
And then paginate simply by going:
Article.feed.per(page_size).page(page)
The biggest advantage of this is that Kaminari can chain into the generated SQL inserting the proper LIMIT and OFFSET clauses thereby reducing the size of the result set returned to only what needs to be displayed, as opposed to returning every matching record.
I think Will Paginate will help you out here -> mislav/will_paginate.
From there you can simply give your controller action .per_page(20) for example and after 20 objects (you can define the objects, see the wiki) there will be pagination

Get db records from today and tomorrow in Rails?

My desired end result is that I want to group events on the events#index page by 3 time periods: Today, Tomorrow and This week. Ideally, it should be sorted by the visitor's time zone, but, the primary use case is EST if that matters.
It's just a fairly simple Rails app that you can check out here:
https://github.com/moizk/events
I'm fairly new to Rails, so I assume this sort of thing is handled by the Controller, and creating three instance variables instead of:
#events = Event.all
It should be something like:
#todaysevents = Events.where(some logic in here)
#tomorrowsevents = Events.where(some logic in here)
I just can't figure out what that logic should be.
I would recommend adding three scopes to the model, replacing event_start_at with whatever time field you are using:
class Event < ActiveRecord::Base
scope :today, lambda { where(event_start_at: Time.zone.now.all_day }
scope :tomorrow, lambda { where(event_start_at: 1.day.from_now.all_day }
scope :this_week, lambda { where(event_start_at: Time.zone.now.all_week }
end
And using them in the controller:
#events_today = Event.today
#events_tomorrow = Event.tomorrow
#events_this_week = Event.this_week
If you follow Sandi Metz's "one instance variable per action" rule, which I quite like, you might prefer to wrap these events in an object:
#events = {
today: Event.today,
tomorrow: Event.tomorrow,
this_week: Event.this_week
}
And then use them in the view like #events[:today], #events[:tomorrow], #events[:this_week].
You could even wrap the hash in an OpenStruct to allow #events.today, #events.tomorrow, #events.this_week, but this is probably well into bike shedding territory.

Scoped and scope in rails

Can somebody explain what this method does and what I can pass to it?
scoped(options = nil)
Returns an anonymous scope.
And also what the scope method does? I don't understand after reading the documentation.
In ActiveRecord, all query building methods (like where, order, joins, limit and so forth) return a so called scope. Only when you call a kicker method like all or first the built-up query is executed and the results from the database are returned.
The scoped class method also returns a scope. The scope returned is by default empty meaning the result set would not be restricted in any way meaning all records would be returned if the query was executed.
You can use it to provide an "empty" alternative like in the query_by_date example by MurifoX.
Or you can use it to combine multiple conditions into one method call, like for example:
Model.scoped(:conditions => 'id < 100', :limit => 10, :order => 'title ASC')
# which would be equivalent to
Model.where('id < 100').limit(10).order('title ASC')
The scope class method allows you to define a class method that also returns a scope, like for example:
class Model
scope :colored, lambda {|col|
where(:color => col)
}
end
which can be used like this:
Model.colored
The nice thing with scopes is that you can combine them (almost) as you wish, so the following is absolutely possible:
Model.red.where('id < 100').order('title ASC').scoped(:limit => 10)
I also strongly suggest reading through http://guides.rubyonrails.org/active_record_querying.html
I have used it in the past.When you make chained calls to the ActiveRecord query interface like this:
Model.where(:conditions).where(:more_conditions).where(:final_conditions)
Each one of them is already scoped, making the chain work without any problems. But let's say you have something like this:
Model.query_by_date(date).query_by_user(user).query_by_status(status)
scope :query_by_date, lambda { |date|
case date
when "today"
where(:date => Date.today)
when "tomorrow"
where(:date => Date.tomorrow)
else
# Any value like '' or 0 or Date.whatever
end
}
This would cause an error if the date param is not today or tomorrow. It would pick the last value and try to chain this query with the next one query_by_user, resulting in a undefined method default_scoped? for ''. But if you put a scoped method in the else condition, it would work without any flaws, because you are saying to activerecord that you pass through this method/named scope and didn't make any calls to where/find/other activerecord methods, but returned a scoped object, so you can continue chaining queries and stuff.
It would be this way in the end.
else
scoped
end
Hope you understand this simple example.

Reusing named_scope to define another named_scope

The problem essence as I see it
One day, if I'm not mistaken, I have seen an example of reusing a named_scope to define another named_scope. Something like this (can't remember the exact syntax, but that's exactly my question):
named_scope :billable, :conditions => ...
named_scope :billable_by_tom, :conditions => {
:billable => true,
:user => User.find_by_name('Tom')
}
The question is: what is the exact syntax, if it's possible at all? I can't find it back, and Google was of no help either.
Some explanations
Why I actually want it, is that I'm using Searchlogic to define a complex search, which can result in an expression like this:
Card.user_group_managers_salary_greater_than(100)
But it's too long to be put everywhere. Because, as far as I know, Searchlogic simply defines named_scopes on the fly, I would like to set a named_scope on the Card class like this:
named_scope from_big_guys, { user_group_managers_salary_greater_than(100) }
- this is where I would use that long Searchlogic method inside my named_scope. But, again, what would be the syntax? Can't figure it out.
Resume
So, is named_scope nesting (and I do not mean chaining) actually possible?
You can use proxy_options to recycle one named_scope into another:
class Thing
#...
named_scope :billable_by, lambda{|user| {:conditions => {:billable_id => user.id } } }
named_scope :billable_by_tom, lambda{ self.billable_by(User.find_by_name('Tom').id).proxy_options }
#...
end
This way it can be chained with other named_scopes.
I use this in my code and it works perfectly.
I hope it helps.
Refer to this question raised time ago here at SO.
There is a patch at lighthouse to achieve your requirement.
Rails 3+
I had this same question and the good news is that over the last five years the Rails core team has made some good strides in the scopes department.
In Rails 3+ you can now do this, as you'd expect:
scope :billable, where( due: true )
scope :billable_by_tom, -> { billable.where( user: User.find_by_name('Tom') ) }
Invoice.billable.to_sql #=> "... WHERE due = 1 ..."
Invoice.billiable_by_tom.to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."
FYI, Rails 3+ they've renamed named_scope to just scope. I'm also using Ruby 1.9 syntax.
Bonus Round: Generic Scope.
If there are multiple people that are "billable" besides just "Tom" then it might be useful to make a generic scope that accepts a name param that gets passed into the block:
scope :billable_by, lambda { |name| billable.where( user: User.find_by_name( name ) ) }
Then you can just call it with:
Invoice.billable_by( "Tom" ).to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."
Btw, you can see I used the older lambda syntax in the bonus round. That's because I think the new syntax looks atrocious when you're passing a param to it: ->( name ) { ... }.
Chain Scopes.
Why not have a scope for stuff just by Tom in general, like:
scope :by_tom, where( user: User.find_by_name('Tom') )
And then you can get those records that are "billable by Tom" with:
Record.billable.by_tom
You can use a method to combine some named_scope like :
def self.from_big_guys
self.class.user_group_managers_salary_greater_than(100)
end
This feature is add on Rails 3 with new syntax (http://m.onkey.org/2010/1/22/active-record-query-interface)

Resources