Updating attribute based on time - Rails - ruby-on-rails

I building a rewards system for a coffee shop. Basically a customer can sign up for a year subscription. Right now when they sign up the active attribute is toggled to true. I'm trying to write a method that will toggle the attribute to false after a year passes. I have a method right now that I want to use but I don't know where to use it at? I also have a failing test. I'll show my current code for clarity.
Controller:
def create
#subscriber = Subscriber.new(subscriber_params)
if #subscriber.save
#subscriber.touch(:subscription_date)
#subscriber.update(active: true)
SubscriberMailer.welcome_subscriber(#subscriber).deliver_now
flash[:notice] = "Subscriber Has Been Successfully Created"
redirect_to new_subscriber_path(:subscriber)
else
render "new"
end
end
Model method I want to use:
def not_active(subscriber)
if subscription_date < 1.year.ago
self.update(active: false)
end
end
Failing Test:
it "sets active to false after a year" do
subscriber = create(:subscriber)
subscriber.update(active: true)
Time.now + 366.days
expect(subscriber.active).to eq(false)
end
So hopefully this idea is clear. I just want to update to active: false if the user was created over a year ago.

You must run the not_active method in order for the method to have an effect. The method has no way of knowing what the date is today and updating a subscriber unless it is actually run. I agree with matt that you would likely run this method in a sidekiq job daily on on all of your subscribers who subscribed a year or longer ago and are active (You can write a scope for this). This way you can call the not_active method and set each subscriber's active appropriately, or write it as a Subscriber class method and apply it to the results of your scope. In the case of testing the not_active method itself all you need to do is call it and test the result. Its also not clear to me why the not_active method takes a subscriber as an arg, it seems like it would make more sense to just call it from a subscriber instance. Is this not whats already happening? I would personally call this method something like deactivate!, as its making changes. not_active kind of sounds like it would return a boolean or an inactive subscriber. I would also recommend using update! instead of update in not_active. update! will raise an error if the update fails. Adding to time.now does actually change the time. You can use rspec mocks to fake the current time if you need to. In any case here is what your not_active test might look like:
it "sets active to false after a year" do
subscriber = Subscriber.create(subscription_date: (1.year.ago - 1.day), active: true)
#changed not_active to deactivate, called from instance instead of passing in subscriber
subscriber.deactivate!
expect(subscriber.active?).to eq(false)
end
You can also write a test for the other case
it "does not deactivate a recent subscriber" do
subscriber = Subscriber.create(subscription_date: Date.today, active: true)
subscriber.deactivate!
expect(subscriber.active?).to eq(true)
end

A simple solution to this would be to use cron. There is a rubygem to interface with cron, called whenever. The setup is simple and well documented.
With cron setup on your server, you would create some kind of class method that would iterate through Subscribers, calling the not_active method.
Btw, if the not_active method is defined within your Subscriber model, you won't need to pass subscriber as an argument, as self will be implicitly set to the subscriber.
The code would end up looking something like:
in subscriber.rb
def self.set_subscribers_to_inactive
find_each(active: false) do |subscriber|
subscriber.inactive!
end
end
def inactive!
update(active: false) if subscription_date < 1.year.ago
end
in schedule.rb
every 1.day do
runner "Subscriber.set_subscribers_to_inactive"
end
As mentioned, your test is not actually calling the not_active method.
it "sets active to false after a year" do
last_year = DateTime.current - 366.days
subscriber = create(:subscriber, active: true, subscription_date: last_year)
subscriber.inactive!
expect(subscriber.active).to eq false
end

Take a look at cron and whenever gem which works on top of cron. You just need to write a super simple script which will extract data from DB and update it.
Another way to solve your problem is not to update anything. You only need *_expires_at column and check if its value less than current date.
It is pretty agile method, because by using activation_expires_at column you are able to implement #active? method and .active scope to select only users with active subscriptions.

Related

cancelling a sheduled Sidekiq job in Rails

Some Sidekiq jobs in my app are scheduled to change the state of a resource to cancelled unless a user responds within a certain timeframe. There is a lot of information about how to best accomplish this task, but none of it actually cancels the job.
To cancel a job, the code in the wiki says:
class MyWorker
include Sidekiq::Worker
def perform(thing_id)
return if cancelled?
thing = Thing.find thing_id
thing.renege!
end
def cancelled?
Sidekiq.redis {|c| c.exists("cancelled-#{jid}") }
end
def self.cancel!(jid)
Sidekiq.redis {|c| c.setex("cancelled-#{jid}", 86400, 1) }
end
end
Yet here it's suggested that I do something like
def perform(thing_id)
thing = Thing.find thing_id
while !cancel?(thing)
thing.ignore!
end
end
def cancel?(thing_id)
thing = Thing.find thing_id
thing.matched? || thing.passed?
end
What's confusing about this and similar code on the wiki is none of it actually cancels the job. The above example just performs an update on thing if cancelled? returns false (as it should), but doesn't cancel if and when it returns true in the future. It just fails with an aasm transition error message and gets sent to the RetrySet. Calling MyWorker.cancel! jid in model code throws an undefined variable error. How can I access that jid in the model? How can actually cancel or delete that specific job? Thanks!
# The wiki code
class MyWorker
include Sidekiq::Worker
def perform(thing_id)
return if cancelled?
# do actual work
end
def cancelled?
Sidekiq.redis {|c| c.exists("cancelled-#{jid}") }
end
def self.cancel!(jid)
Sidekiq.redis {|c| c.setex("cancelled-#{jid}", 86400, 1) }
end
end
# create job
jid = MyWorker.perform_async("foo")
# cancel job
MyWorker.cancel!(jid)
You can do this but it won't be efficient. It's a linear scan for find a scheduled job by JID.
require 'sidekiq/api'
Sidekiq::ScheduledSet.new.find_job(jid).try(:delete)
Alternatively your job can look to see if it's still relevant when it runs.
Ok, so turns out I had one question already answered. One of the code sets I included was a functionally similar version of the code from the wiki. The solution to the other question ("how can I access that jid in the model?") seems really obvious if you're not still new to programming, but basically: store the jid in a database column and then retrieve/update it whenever it's needed! Duh!

Can I call delayed_job with max attempts of 1?

I have a method that I run asynchronously
User.delay(queue: 'users').grab_third_party_info(user.id)
In case this fails, I want it to not retry. My default retries are 3, which I cannot change. I just want to have this only try once. The following doesn't seem to work:
User.delay(queue: 'users', attempts: 3).grab_third_party_info(user.id)
Any ideas?
This isn't my favorite solution, but if you need to use the delay method that you can set the attempts: to one less your max attempts. So in your case the following should work
User.delay(queue: 'users', attempts: 2).grab_third_party_info(user.id)
Better yet you could make it safer by using Delayed::Worker.max_attempts
User.delay(queue: 'users', attempts: Delayed::Worker.max_attempts-1).grab_third_party_info(user.id)
This would enter it into your delayed_jobs table as if it already ran twice so when it runs again it will be at the max attempts.
From https://github.com/collectiveidea/delayed_job#custom-jobs
To set a per-job max attempts that overrides the Delayed::Worker.max_attempts you can define a max_attempts method on the job
NewsletterJob = Struct.new(:text, :emails) do
def perform
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
end
def max_attempts
3
end
end
Does this help you?
You have to use a Custom Job.
Just like #lazzi showed, you have to create a custom job in order to override the max_attempts.
As you can see in the README here, the only params that the .delay method take are:
priority
run_at
queue
And if you think about it, a value for max_attempts is not stored in the delayed_jobs table, only the attempts are stored, so there's no way for it to be persisted.
The only way to do it is to create a custom job that gets re-instantiated when the delayed job worker processes the job. It then reads the value from the max_attempts method and uses that to determine if the current attempts in the table record equals or exceeds the max_attempts value.
In your case, the simplest way to do it would be something like this:
# Inside your user.rb
class User < ApplicationRecord
FetchThirdPartyInfoJob = Struct.new( :user ) do
def perform
User.grab_third_party_info(user.id) # REFACTOR: Make this an instance method so you don't need to pass the User's id to it.
end
def queue_name
"users"
end
def max_attempts
3
end
end
end
Then run it wherever you need to by using enqueue, like this:
Delayed::Job.enqueue( User::FetchThirdPartyInfoJob.new( user ) )
I also added a little REFACTOR comment on your code because User.grab_third_party_info(user.id) looks to be incorrectly setup as a class method that you then pass the instance id to instead of just calling it directly on the user instance. I can't think of a reason why you would want this, but if there is, please leave it in the comments so we can all learn.

How can I call a controller action from ActiveAdmin?

I have this method in my reports_controller.rb, which allows an user to send a status.
def send_status
date = Date.today
reports = current_user.reports.for_date(date)
ReportMailer.status_email(current_user, reports, date).deliver
head :ok
rescue => e
head :bad_request
end
How can I call this action from ActiveAdmin, in order to check if a User sent this report or not? I want it like a status_tag on a column or something.
Should I do a member action?
Thanks!
I'll address the issue of checking if a report has been sent later, but first I'll cover the question of how to call the controller action from ActiveAdmin.
While you can call ReportsController#send_status by creating an ActionController::Base::ReportsController and then calling the desired method, e.g.
ActionController::Base::ReportsController.new.send_status
this isn't a good idea. You probably should refactor this to address a couple potential issues.
app/controllers/reports_controller.rb:
class ReportsController < ApplicationController
... # rest of controller methods
def send_status
if current_user # or whatever your conditional is
ReportMailer.status_email(current_user).deliver
response = :ok
else
response = :bad_request
end
head response
end
end
app/models/user.rb:
class User < ActiveRecord::Base
... # rest of user model
def reports_for_date(date)
reports.for_date(date)
end
end
app/mailers/reports_mailer.rb
class ReportsMailer < ActionMailer::Base
... # rest of mailer
def status_email(user)
#user = user
#date = Date.today
#reports = #user.reports_for_date(#date)
... # rest of method
end
end
This could obviously be refactored further, but provides a decent starting point.
An important thing to consider is that this controller action is not sending the email asynchronously, so in the interest of concurrency and user experience, you should strongly consider using a queuing system. DelayedJob would be an easy implementation with the example I've provided (look into the DelayedJob RailsCast).
As far as checking if the report has been sent, you could implement an ActionMailer Observer and register that observer:
This requires that the User model have a BOOLEAN column status_sent and that users have unique email address.
lib/status_sent_mail_observer.rb:
class StatusSentMailObserver
self.delivered_email(message)
user = User.find_by_email(message.to)
user.update_attribute(:status_sent, true)
end
end
config/intializer/setup_mail.rb:
... # rest of initializer
Mail.register_observer(StatusSentMailObserver)
If you are using DelayedJob (or almost any other queuing system) you could implement a callback method to be called on job completion (i.e. sending the status email) that updates a column on the user.
If you want to track the status message for every day, you should consider creating a Status model that belongs to the User. The status model could be created every time the user sends the email, allowing you to check if the email has been sent simply by checking if a status record exists. This strategy is one I would seriously consider adopting over just a simple status_sent column.
tl;dr ActionController::Base::ReportsController.new.send_status & implement an observer that updates a column on the user that tracks the status. But you really don't want to do that. Look into refactoring like I've mentioned above.

Custom Model Method, setting scope for automatic sending of mail

There are several stages to this, and as I am relatively new to rails I am unsure if I am approaching this in the best way.
Users follow Firms, Firms applications open and close on certain days. If a user follows a firm I would like them to automatically get an email when a) the firms application opens, b) a week before the firms applications close, c) on the day that the firms applications close.
I have tried using named scope. I have the following model method (I presume this will need a little work) setting each firms scope, depending on the date.
model firms.rb
def application_status
if open_date == Today.date
self.opening = true
else
self.opening = false
end
if ((close_day - Today.date) == 7)
self.warning = true
else
self.warning = false
end
if close_day == Today.date
self.closing = true
else
self.closing = false
end
end
I would like this method to be called on each firm once a day, so that each firm has the appropriate scope - so I have tried using the whenever gem (cron) and the following code. Running the above model method on each firm.
Schedule.rb
every 1.day do
runner "Firm.all.each do |firm|
firm.application_status
end"
end
Then for each of the scopes opening, warning, closing i have a method in the whenever schedules file, For simplicity I shall show just the opening methods. The following queries for all firms that have had the opening scope applied to them, and runs the application_open_notification method on them.
Schedule.rb
every 1.day do
runner "Firm.opening.each do |firm|
firm.application_open_notification
end"
end
This calls the following method in the Firm.rb model
def application_open_notification
self.users.each do |user|
FirmMailer.application_open(user, self).deliver
end
end
Which in turn calls the final piece of the puzzle... which should send the user an email, including the name of the firm.
def application_open(user,firm)
#firm = firm
#user = user
mail to: #user.email, subject: #firm' is now accepting applications'
end
end
Is this a viable way to approach this problem? In particular I am not very familiar with coding in the model.
Many thanks for any help that you can offer.
I'll guess that opening, warning and closing are database fields, and you have scopes like:
class Firm < ActiveRecord::Base
scope :opening, :where => { :opening => true }
# etc
end
There is a general rule for database (and, well, all storage): don't store things you can caculate, if you don't have to.
Since an application's status can be dermined from the day's date and the open_date and close_day fields, you could calculate them as needed instead of creating extra fields for them. You can do this with SQL and Active Record:
scope :opening, :where { :open_date => (Date.today .. Date.today+1) }
scope :warning, :where { :close_day => (Date.today+7 .. Date.today+8) }
scope :closing, :where { :close_day => (Date.today .. Date.today+1) }
(Note that these select time ranges. They may have to be changed depending on if you are using date or time fields.)
But there is another issue: what happens if, for some reason (computer crash, code bug etc) your scheduled program doesn't run on a particular day? You need a way of making sure notices are sent eventually even if something breaks. There are two solutions:
Write your schedule program to optionally accept a date besides today (via ARGV)
keep flags for each firm for whether each kind of notice has been sent. These will have to be stored in the databse.
Note that scopes aren't necessary. You are able to do this:
Firm.where(:open_date => (Date.today .. Date.today+1)).each do |firm|
#...
end
but the scope at least encapsulates the details of identifying the various sets of records.

Calling a model method at a specific time (Ruby on Rails)

I have a Ruby on Rails model that has a column called expiration_date. Once the expiration date is reached, I want another column on the model to be modified (e.g. expired = true). What are some good ways of doing this?
Ideally I'd like a model function to be called at the exact moment the expiry date is reached.
Use delayed_job gem. After installing delayed_job gem do the following:
class Model < ActiveRecord::Base
after_create :set_expiry_timer
# register the timer
def set_expiry_timer
delay(:run_at => expiration_date).expire
end
def expire
update_attribute(:expired, true) unless expired?
end
end
For the scenario you describe, the best solution is to have an expired method instead of a column, that would return true iff the expiration_date is greater or equal than the current date.
For other scenarios, I would go with a DB scheduled event triggering a stored procedure. That procedure would check the expiration_date column for all the rows in the model table, and update the expired (or other(s)) column(s) accordingly.
Have you considered using a scheduler to automate this? Something like Resque, Delayed Job or Cron would work fine.
Then in your scheduled task you could have something like this:
if foo.expiration_date < Time.now
foo.is_expired = true
foo.save
end

Resources