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
Related
`I have recently started working on a rails app and using devise as authentication. but I have ran into a wall. I would like to know if there's a way to set a period limit on how often a user may update their username. For example, if a User update their username today, they shouldn't be able to update it again until a 30day period has passed.
I have looked through devise docs, but nothing address that functionality, I have also search SO and the web but to no avail.
Any help would be very much be appreciated on how to go about it, or at least be pointed in the right direction. Thanks in advance!
I have added to the user model
after_save :name_last_updated
def name_last_updated
if self.username_changed?
self.name_last_updated_at = Time.now
end
but this does not update the colunm name_last_updated_at. any clues of what i am doing wrong will be helpful ^^ thanks!
so, after messing around with a few codes i figured an alternative way to go about this. I created a new column to track new time the user should be able to see the form to update his username.
def username_next_update
self.username_next_update_at = self.username_last_update_at + 2.minutes
end
for learning and testing purposes i added +2.minutes
and i did a before_save on it.
In my view i wrapped it around an if and else statement although i am quite confuse with the logic.
<% if current_user.username_next_update_at < time.zone.now %>
********
<% end %>
but i expected to it to work only if it was > sign instead of <. any tips will be helpful or any better alternatives :)
You need to create it from scratch.
Create a new column in the users table, call it name_last_updated, when the user updates their name for the first time, set that column to today's date. then every time a user wants to update their name, check that column and compare with today's date and see if 30 days have passed:
if Date.today - user.name_last_update < 30
#display error
end
We can use user updated_at column created by the devise gem and add a callback method in the user model to make sure that we call this method every time the user model is updated.
before_update { |user| user.write_attribute if user.is_permitted? }
def write_attribute
self.user_name = params[:user][:user_name]
end
def is_permitted?
if self.username_changed?
Date.today - updated_at < 30
end
end
You should use the before_update and specify which record has to activate the callback instead of using after_save.
I would do something like this:
before_update :name_last_updated, if: :username_changed?
def name_last_updated
if (self.name_last_updated_at.to_date + 30.days) < Date.today
self.update(name_last_updated_at: Time.now)
end
end
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.
I have a boolean in the DB: t.boolean "completed", default: false
I ONLY show those still false on the home page.
If a user checks one off:
<span class="glyphicon glyphicon-ok"><% :completed %></span>
Completed becomes true and it therefore disappears from the home page.
At the end of each day though, how can we automatically reset everything back to default: false?
Do I create a model method or scope to do this?
I would use a timestamp instead of a boolean. That has two benefits:
You know when the user complated the last time
You do not need to reset that value via a cron job at midnight.
The steps would be: First remove the completed boolean column from your database and add instead a completed_at timestamp column.
Second add two methods to your model that allow the same behaviour than the boolean column before. That means there is not need to change the controller or views:
def completed=(boolean)
self.completed_at = boolean ? Time.current : nil
end
def completed
completed_at && completed_at >= Time.current.beginning_of_day
end
I think you will actually want a rake task to reset them all, then just run it once a day with cron. Have a look at the whenever gem.
You CAN do it without the rake task, but its a bit more difficult and inefficient, you will have to add a completed_timestamp, and set that to the current time whenever it is set to true. Then, whenever you fetch the model, check if the completed_timestamp is before today, and if it is, set it to false before you render your page. You could probably do this in an after_find callback.
Edit: I highly recommend you go with the cron+rake task solution though, the second method is highly inefficient because you will have to fetch every record and check its timestamp every time you load the page. Its just a terrible solution, but I added it for completeness.
You can create a rake task using whenever gem. Something like below should work
every 1.day, :at => '12:00 am' do
runner "YourModel.method_to_update"
end
And in your model, write a method like below
def self.method_to_update
YourModel.update_attribute(completed: false)
end
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.
This is my user model and I don't know how can I write test for active_and_approved_creative_count cache test.
User.rb
class User < ActiveRecord::Base
class << self
def active_and_approved_creative_count
Rails.cache.fetch('active_and_approved_creative_count', :expires_in => 30.minutes) do
User.active_and_approved_creative.count
end
end
...
scope :active_and_approved_creative ,where("user_type = ? AND (membership_cancelled IS NULL OR membership_cancelled = false)", :approved_creative)
end
You could probably:
access active_and_approved_creative_count to prime the cache and verify the initial value
perform an action that will increment the count
Use TimeCop to move up 29 minutes
Verify that the count is still the cached value
Travel up another minute or two
Verify that the count has now incremented
One might argue that this is testing the Rails internals unnecessarily, consider whether simply performing step #1 may be enough.