What is the best way to reuse a scope in Rails? - ruby-on-rails

I'm confused to reuse or writing a new scope.
for example,
one of my methods will return future subscription or current subscription or sidekiq created subscriptions.
as scopes will look like:
scope :current_subscription, lambda {
where('(? between from_date and to_date) and (? between from_time and to_time)', Time.now, Time.now)
}
scope :sidekiq_created_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is not null")
}
scope :future_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is null")
}
so these were used for separate purposes in different methods, so for me what I tried is to check whether a particular account record will come under which of three subscriptions.
so I tried like:
def find_account_status
accounts = User.accounts
name = 'future' if accounts.future_subscription.where(id: #account.id).any?
name = 'ongoing' if accounts.current_subscription.where(id: #account.id).any?
name = 'sidekiq' if accounts.sidekiq_enqued_subscription.where(id: #account.id).any?
return name
end
so here what my doubt is, whether using like this is a good way, as here we will be fetching the records based on the particular subscriptions and then we are checking whether ours is there or not.
can anyone suggest any better way to achieve this?

Firstly, you are over using the scopes here.
The method #find_account_status will execute around 4 Queries as below:
Q1 => accounts = User.accounts
Q2 => accounts.future_subscription
Q3 => accounts.current_subscription
Q4 => accounts.sidekiq_enqued_subscription
Your functionality can be achived by simply using the #account object which is already present in memory as below:
Add below instance methods in the model:
def current_subscription?
# Here I think just from_time and to_time will do the work
# but I've added from_date and to_date as well based on the logic in the question
Time.now.between?(from_date, to_date) && Time.now.between?(from_time, to_time)
end
def future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].blank?
end
def sidekiq_future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].present?
end
#find_account_status can be refactored as below:
def find_account_status
if #account.current_subscription?
'ongoing'
elsif #account.future_subscription?
'future'
elsif #account.sidekiq_future_subscription?
'sidekiq'
end
end
Additionally, as far as I've understood the code, I think you should also handle a case wherein the from_date and to_date are past dates because if that is not handled, the status can be set based on the field meta["special_sub_enqueued_at"] which can provide incorrect status.
e.g. Let's say that the from_date in the account is set as 31st Dec 2021 and meta["special_sub_enqueued_at"] is false or nil.
In this case, #current_subscription? will return false but #future_subscription? will return true which is incorrect, and hence the case for past dates should be handled.

Related

How to check range between two dates with rails 6

Hello i have this blog module where my posts can have 3 states "no_status" "in_draft" and "published", the user can set the publish_date and publish_end_date for his posts
while this range between the dates is fulfilled, the status of the post must be "published", and when it is finished return to "in_draft"
def post
if self.no_status? || self.in_draft?
if self.publish_date >=Date.today && self.publish_end <= Date.today
self.update_attribute :status, 'published'
end
elsif self.published?
if self.publish_date.past? && self.publish_end.past?
self.update_attribute :status, 'in_draft'
end
end
end
What is the proper way to manage this, i have a big problem with my conditions.
In the 1st branch your conditionals are mixed up now, they should be opposite (that's why it doesn't work as expected - you check than publish_date is greater that current date, but this is wrong - it must be in the past to have the post published today). So if you simply "mirror" your conditional operators it should work - but there are cleaner ways of writing the same:
Date.today.between?(publish_date, publish_end)
# or
(publish_date..publish_end).cover?(Date.today)
In the 2nd branch checking that pulish_end date is n the past should be enough. Checking if publish_date is in the past too is redundant - if it is not you have bigger problems what just a wrong status :) - this kind of basic data integrity is better to be addressed by model validations.
Also, the nested ifs are absolutely unnecessary here, they just make the code harder to reason about.
To summarize, something like the following should do the job (I'm not discussing here how this method is being used and whether it should be written this way or not - just addressing the initial question)
def post
new_status =
if published? && publish_end.past?
'in_draft'
elsif Date.today.between?(publish_date, publish_end)
'published'
end
update_attribute :status, new_status
end
You can use Object#in?
def post
if no_status? || in_draft?
update(status: 'published') if Date.today.in?(publish_date..publish_end)
elsif published?
update(status: 'in_draft') if publish_date.past? && publish_end.past?
end
end
(BTW you don't need self in Ruby every time)
You can use Range#cover?. It basically takes a range and checks if the date is withing the start/end;
(10.days.ago..1.day.ago).cover?(3.days.ago)
# true
So, in your case;
(publish_date..publish_end).cover?(Date.today)

Rails Query a List for a CRON Job

I'm a complete novice with CRON jobs but I think I have that set up correctly.
Ultimately what I'm trying to do is send an email every day at 8:00 am to users (and a couple others) that have not logged in within the last 3 days, have not received the email, AND are marked as active OR temp as a status.
So from querying the db in console I know that I can do:
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
That's not certainly clean but I was struggling with finding a contained query that grabbed all that data. Is there a cleaner way to do this query?
I believe I have my cron job set up correctly using a runner. I have whenever installed and in my schedule.rb I have:
every 1.day, at: '8:00 am' do
runner 'ReminderMailer.agent_mailer.deliver'
end
So under app > mailer I created ReminderMailer
class ReminderMailer < ApplicationMailer
helper ReminderHelper
def agent_reminder(user)
#user = user
mail(to: email_recipients(user), subject: 'This is your reminder')
end
def email_recipients(agent)
email_address = ''
email_addresses += agent.notification_emails + ',' if agent.notification_emails
email_addresses += agent.manager
email_address += agent.email
end
end
Where I'm actually struggling is where I should put my queries to send to the mailer, which is why I built a ReminderHelper.
module ReminderHelper
def applicable_agents(user)
agent = []
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
agent << third
return agent
end
end
EDIT: So I know I could in theory do a chain of where queries. There's gotta be a better way right?
So what I need help on is: do I have the right structure in place? Is there a cleaner way to query this data in ActiveRecord for the CRON job? Is there a way to test this?
Try combining them together as if understand the conditions correct
Have not logged in within the last 3 days,
Have not received the email
Are marked as active OR temp as a status
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
module ReminderHelper
def applicable_agents(user)
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
end
end
You don't need to add/ assign them to array. Because this relation is already like an array. You can use .to_a if you need array. If you just want to iterate over them then users.each should work fine.
Update
class User
scope :not_notified, -> { where(notified: false) }
scope :active_or_temp, -> { where(status: ['active', 'temmp']) }
scope :last_login_in, -> (default_days = 3) { where("last_login_at < ?", default_days.days.ago) }
end
and then use
User.not_notified.active_or_temp.last_login_in(3)
Instead of Time.now-3.days it's better to use 3.days.ago because it keeps time zone also in consideration and avoids unnecessary troubles and failing test cases.
Additionally you can create small small scopes and combine them. More read on scopes https://guides.rubyonrails.org/active_record_querying.html

Rails validations and initializations with interdependent fields

After coding in Rails for a couple of years, I still don't understand the Rails Way for more advanced concepts. The Rails Way is so specific about convention over configuration, however when you get into Enterprise coding, all rules go out the window and everyone writes blogs with non-standard way of doing things. Here's another one of those situations: contextual validations where the context is a little more complex (fields depend on each other). Basically in a shipping application, I need to initialize an AR object with request params and with some calculated values. The calculated values are dependent on the request params, and I'm not sure how to initialize and validate my member variables.
table mailpieces
mail_class
weight
sort_code
t_indicator_id
end
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def some_kind_of_initializer
if mail_class == 'Priority'
sort_code = '123'
elsif mail_class == 'Express'
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
t_indicator = ndicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
mailpiece = Mailpiece.new(
mail_class: params[:mail_class],
weight: params[:weight])
#mailpiece.some_kind_of_initializer ?!
raise 'SomeError' if !mailpiece.valid?
What should some_kind_of_initializer be?
Override of ActiveRecord initialize? That's not good practice.
after_initialize. More Rails Way-sy.
Custom method called after
Mailpiece.new (e.g. mailpiece.some_kind_of_initializer)
Whichever of the above choices, the problem is that the initialization of sort_code and t_indicator depends on mail_class and weight being valid. Given that mail_class and weight should be not null before I enter some_kind_of_initializer, how should I write my validations?
Extract all validations into a json schema validation. More complex business rules around mail_class and weight are difficult to write in a json schema.
Extract all validations into some type of Data Transfer Object validation class. Moves away from the Rails Way of doing things. Feels like I'm writing in .NET/Java and I'm afraid that Rails will kick my azz later (in validations, testing, etc.).
Assign sort_code only if mail_class and weight have been initialized. This seems to be most Rails Way to write things, but it's tough. So many if/else. This is just a simple example, but my mailpiece has references that have references and they all do these type of validations. If this is the right answer, then I'm getting a gut feeling that it might be easier to move ALL validations and ALL initializations to an external class/module - perhaps getting close to option #2.
Option 3 code rewrite
def some_kind_of_initializer
if mail_class && weight
if (mail_class == 'Priority')
sort_code = '123'
elsif (mail_class == 'Express')
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
end
if sort_code
t_indicator = Indicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
I'd love to hear your opinion on this. It seems to me this is such a popular use case of AR and I'm not sure what to do. Again, this is a just a simple case. My Mailpiece model has many other references that have dependencies on the mailpiece object properties and their own interdependencies in the same style as above.
I'm not sure if it helps or I understood your question correctly, but maybe using "case" instead of if/elsif/else, the ternary operator, some lazy assignment and some extra validations can give your code a better "rails" feeling.
With lazy assignment I mean that you could wait to initialize sort_code and t_indicator until you actually need to use it or save it.
def sort_code
return self[:sort_code] if self[:sort_code].present?
sort_code = case mail_class
when 'Priority' then '123'
when 'Express'
weight < 1 ? '456' : '789' if weight
end
self[:sort_code] = sort_code
end
That way, sort_code gets initialized right before you need to use it the first time so you can do Mailpiece.new(mail_class: 'Something') and forget about initializing sort_code right away.
The problem would come when you need to save the object and you never called mailpiece.sort_code. You could have a before_validation callback that justs calls sort_code to initialize it in case it wasn't already.
The same can be done for t_indicator.
I would also add some context in your validations
validates_presence_of :weight, if: Proc.new{|record| 'Express' == record.mail_class} #you could add some helper method "is_express?" so you can just do "if: :is_express?"
validates_presence_of :sort_code, if: Proc.new{|record| 'Priority' == record.mail_class or 'Express' == record.mail_class && record.weight.present?}
Sorry if I missunderstood your question, I didn't even use your options haha.
How about:
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def default_mail_class
'Priority'
end
def default_sort_code
mail_class = mail_class || default_mail_class
if mail_class == 'Priority'
'123'
elsif mail_class == 'Express'
weight < 1 ? '456' : '789'
end
end
end
Then, when you needed to figure out t_indicator, just do it as-needed:
def is_foobar_indicator?
sort_code || default_sort_code == '456'
end
def t_indicator
indicator_params = {name: is_foobar_indicator? ? 'foobar' : 'blah'}
Indicator.find_by(indicator_params)
end
So you can still have the validation on sort_code (assuming that is user provided) but still use defaults when looking at t_indicator. I don't know how complicated the rest of your model is, but I would suggest not looking up values until you need them. Also, after_initialize is risky because it runs your code exactly where it claims - after every initialize, so if you run a query for N items but do nothing but find on them, or wind up not using the defaults you're setting, after_initialize runs N times for nothing.

Ignore parameters that are null in active record Rails 4

I created a simple web form where users can enter some search criteria to look for venues e.g. a price range. When a user clicks "find" I use active record to query the database. This all works very well if all fields are filled in. Problems occur when one or more fields are left open and therefore have a value of null.
How can I work around this in my controller? Should I first check whether a value is null and create a query based on that? I can imagine I end up with many different queries and a lot of code. There must be a quicker way to achieve this?
Controller:
def search
#venues = Venue.where("price >= ? AND price <= ? AND romance = ? AND firstdate = ?", params[:minPrice], params[:maxPrice], params[:romance], params[:firstdate])
end
You may want to filter out all of the blank parameters that were sent with the request.
Here is a quick and DRY solution for filtering out blank values, triggers only one query of the database, and builds the where clause with Rails' ActiveRecord ORM.
This approach safeguards against SQL-injection, as pointed out by #DanBrooking. Rails 4.0+ provides "strong parameters." You should use the feature.
class VenuesController < ActiveRecord::Base
def search
# Pass a hash to your query
#venues = Venue.where(search_params)
end
private
def search_params
params.
# Optionally, whitelist your search parameters with permit
permit(:min_price, :max_price, :romance, :first_date).
# Delete any passed params that are nil or empty string
delete_if {|key, value| value.blank? }
end
end
I would recommend to make method in Venue
def self.find_by_price(min_price, max_price)
if min_price && max_price
where("price between ? and ?", min_price, max_price)
else
all
end
end
def self.find_by_romance(romance)
if romance
where("romance = ?", romance)
else
all
end
end
def self.find_by_firstdate(firstdate)
if firstdate
where("firstdate = ?", firstdate)
else
all
end
end
And use it in your controller
Venue
.find_by_price(params[:minPrice], params[:maxPrice])
.find_by_romance(params[:romance])
.find_by_firstdate(params[:firstdate])
Another solution to this problem, and I think a more elegant one, is using scopes with conditions.
You could do something like
class Venue < ActiveRecord::Base
scope :romance, ->(genre) { where("romance = ?", genre) if genre.present? }
end
You can then chain those, which would work as an AND if there is no argument present, then it is not part of the chain.
http://guides.rubyonrails.org/active_record_querying.html#scopes
Try below code, it will ignore parameters those are not present
conditions = []
conditions << "price >= '#{params[:minPrice]}'" if params[:minPrice].present?
conditions << "price <= '#{params[:maxPrice]}'" if params[:maxPrice].present?
conditions << "romance = '#{params[:romance]}'" if params[:romance].present?
conditions << "firstdate = '#{params[:firstdate]}'" if params[:firstdate].present?
#venues = Venue.where(conditions.join(" AND "))

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Resources