Friday
Jul312009
Recurring Items in a Rails Application
Friday, July 31, 2009 at 7:50AM
I recently had to implement some recurring items in a Rails application - we'll call them "entries" here though you can use this technique for appointments, bills, or whatever else. After thinking about it for a while, I settled on:
Here's what I came up with. If your needs are different, you'll no doubt need to alter it. First, the schedule model has a schedule_type, an interval, a specific value, and a description. The combination of these things lets me implement
[sourcecode language='ruby']
class Schedule
has_many :entries
attr_accessible :schedule_type, :description, :interval, :specific
SCHEDULE_TYPE = {:first => 0, :last => 1, :weekly => 2, :monthly => 3}
def next_date(current_date)
case schedule_type
when SCHEDULE_TYPE[:first]
first_day = (current_date + 1.month).beginning_of_month
wday = first_day.wday
if specific >= wday
first_day + specific - wday
else
first_day + 7 + specific - wday
end
when SCHEDULE_TYPE[:last]
if specific == -1
(current_date + 1.month).end_of_month
else
last_day = (current_date + 1.month).end_of_month
wday = last_day.wday
if wday >= specific
last_day - wday + specific
else
last_day - wday - 7 + specific
end
end
when SCHEDULE_TYPE[:weekly]
current_date + interval.weeks
when SCHEDULE_TYPE[:monthly]
current_date + interval.months
end
end
[/sourcecode]
So we can have a schedule that is "first Monday of the month" or "every 3 weeks" among other things. I stock these up using db-populate; here's some of the population file:
[sourcecode language='ruby']
Schedule.create_or_update(:id => 1,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 1, :description => "Every month")
Schedule.create_or_update(:id => 2,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 2, :description => "Every other month")
Schedule.create_or_update(:id => 4,
:schedule_type => Schedule::SCHEDULE_TYPE[:weekly],
:interval => 1, :description => "Every week")
Schedule.create_or_update(:id => 8,
:schedule_type => Schedule::SCHEDULE_TYPE[:first],
:specific => 0, :description => "First Sunday of every month")
Schedule.create_or_update(:id => 15,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => 0, :description => "Last Sunday of every month")
Schedule.create_or_update(:id => 22,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => -1, :description => "Last day of every month")
[/sourcecode]
Then the entry model ties into the schedule model. Here are the important (for this purpose) bits of the entry class:
[sourcecode language='ruby']
class Entry < ActiveRecord::Base
belongs_to :schedule
named_scope :ready_to_recur, lambda { |date|
{:conditions => ["recurring = 1 AND next_date <= ? AND schedule_id IS NOT NULL AND next_created = 0", date ]} }
before_save :set_up_recurrence
def make_next_recurrence
if recurring? && !schedule_id.nil? && !next_created?
entry = Entry.create(
:entry_date => next_date,
:reference => reference,
:recurring => true,
:schedule_id => schedule_id
)
update_attribute(:next_created, true)
end
entry
end
def set_up_recurrence
if recurring? && !schedule_id.nil?
self.next_date = Schedule.find(schedule_id).next_date(entry_date)
self.next_created = false if next_created.nil?
end
true
end
end
[/sourcecode]
Finally, the whole thing is driven by a rake task that we run every night. This task finds all the entries that are ready to recur and creates the next entry:
[sourcecode language='ruby']
desc 'Create the recurring entries for today'
task :daily_recurring_entries => :environment do
Entry.ready_to_recur(Date.today).each do |entry|
entry.make_next_recurrence
end
end
[/sourcecode]
- At any point, we needed to know what the next recurrence date was.
- The potential schedules needed to be data-driven (because the client was unsure what would be needed).
Here's what I came up with. If your needs are different, you'll no doubt need to alter it. First, the schedule model has a schedule_type, an interval, a specific value, and a description. The combination of these things lets me implement
Schedule#next_date to return the fate for the next date given a schedule object and the current date. Here's the model:[sourcecode language='ruby']
class Schedule
has_many :entries
attr_accessible :schedule_type, :description, :interval, :specific
SCHEDULE_TYPE = {:first => 0, :last => 1, :weekly => 2, :monthly => 3}
def next_date(current_date)
case schedule_type
when SCHEDULE_TYPE[:first]
first_day = (current_date + 1.month).beginning_of_month
wday = first_day.wday
if specific >= wday
first_day + specific - wday
else
first_day + 7 + specific - wday
end
when SCHEDULE_TYPE[:last]
if specific == -1
(current_date + 1.month).end_of_month
else
last_day = (current_date + 1.month).end_of_month
wday = last_day.wday
if wday >= specific
last_day - wday + specific
else
last_day - wday - 7 + specific
end
end
when SCHEDULE_TYPE[:weekly]
current_date + interval.weeks
when SCHEDULE_TYPE[:monthly]
current_date + interval.months
end
end
[/sourcecode]
So we can have a schedule that is "first Monday of the month" or "every 3 weeks" among other things. I stock these up using db-populate; here's some of the population file:
[sourcecode language='ruby']
Schedule.create_or_update(:id => 1,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 1, :description => "Every month")
Schedule.create_or_update(:id => 2,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 2, :description => "Every other month")
Schedule.create_or_update(:id => 4,
:schedule_type => Schedule::SCHEDULE_TYPE[:weekly],
:interval => 1, :description => "Every week")
Schedule.create_or_update(:id => 8,
:schedule_type => Schedule::SCHEDULE_TYPE[:first],
:specific => 0, :description => "First Sunday of every month")
Schedule.create_or_update(:id => 15,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => 0, :description => "Last Sunday of every month")
Schedule.create_or_update(:id => 22,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => -1, :description => "Last day of every month")
[/sourcecode]
Then the entry model ties into the schedule model. Here are the important (for this purpose) bits of the entry class:
[sourcecode language='ruby']
class Entry < ActiveRecord::Base
belongs_to :schedule
named_scope :ready_to_recur, lambda { |date|
{:conditions => ["recurring = 1 AND next_date <= ? AND schedule_id IS NOT NULL AND next_created = 0", date ]} }
before_save :set_up_recurrence
def make_next_recurrence
if recurring? && !schedule_id.nil? && !next_created?
entry = Entry.create(
:entry_date => next_date,
:reference => reference,
:recurring => true,
:schedule_id => schedule_id
)
update_attribute(:next_created, true)
end
entry
end
def set_up_recurrence
if recurring? && !schedule_id.nil?
self.next_date = Schedule.find(schedule_id).next_date(entry_date)
self.next_created = false if next_created.nil?
end
true
end
end
[/sourcecode]
Finally, the whole thing is driven by a rake task that we run every night. This task finds all the entries that are ready to recur and creates the next entry:
[sourcecode language='ruby']
desc 'Create the recurring entries for today'
task :daily_recurring_entries => :environment do
Entry.ready_to_recur(Date.today).each do |entry|
entry.make_next_recurrence
end
end
[/sourcecode]

Reader Comments (7)
I suppose you know about runt gem
http://runt.rubyforge.org/
The amazing Tracks project also implements recurring items. http://getontracks.org
http://github.com/joevandyk/fixie_events/tree/master is a rails plugin that lets you do recurring events pretty easily with activerecord.
Mike, FWIW, I like your approach much more than the two plugins mentioned.
Very easy to understand. Thanks for sharing!
Very clean way of doing recurring "things". Thanks for sharing. This will be of immediate help to me!
-Ryan
hi, the concept of recurring is what i was looking for i need some idea regarding recurring projects . Here's some brief introduction about my application . People can login into my application and create there own projects/services like filling income tax, it returning etc. i need to apply recurring concept on these projects/services if u could throw some light on it.
It appears that you only create one recurrence at a time instead of all the recurrences at one time. This seems to make it so you couldn't see a recurring meeting on a calendar four months out. Am I missing something?