Recurring Events in Calendar - Rails
Asked Answered
G

4

30

I am searching for the best way to model recurring events. I am using fullcalendar to display events. But I guess recurring events are best handled on the rails backend.

I already looked at other questions and existing example code but I didn't find anything which fits.

It should behave similar like google calendar. So it should be possible to delete/modify single events of the recurring event series. But saving all events of the event series in the database seems inefficient. Also it should be possible to create single events without any recurrence.

What would be a good model architecture?

My event model right now looks like that (without additional attributes):

# Table name: events
#
#  id              :integer         not null, primary key
#  employee_id     :integer
#  created_at      :datetime
#  updated_at      :datetime
#  starts_at       :datetime
#  ends_at         :datetime
#

class Event < ActiveRecord::Base
  attr_accessible :starts_at, :ends_at
end
Gaeta answered 13/4, 2012 at 22:11 Comment(0)
Z
45

Here is how I would model this. I haven't used Google Calendar much, so I'm basing the functionality off of iCal's recurring events.

All models should have the usual id, created_at, updated_at properties. Listed are the custom properties. If the property is another model, you will implement it an association such as has_one or belongs_to.

  • RecurrencePeriod
    • Event base_event # has_one :base_event, :class_name'Event'
    • Time end_date # may be nil, if it recurs forever
    • WeeklyRecurrence recurrence # has_one :recurrence, :as=>:recurrence
    • Array[OccurrenceOverride] overrides # has_many :overrides, :class_name=>'OccurrenceOverride'

The RecurrencePeriod starts on the date that its base_event starts. Also, I assume that an Event's employee_id refers to the employee that created that event. A RecurrencePeriod will also belong to the employee that created the base_event.

The model depends on how flexibly you want to be able to specify recurrences. Are you going to support "Tuesday and Thursday every two weeks from 10 AM to 11 AM and from 2 PM to 3 PM" or just "repeats weekly"? Here's a model that supports just "repeats weekly", "repeats every two weeks", etc.; you can expand it if you need to.

  • WeeklyRecurrence
    • Integer weeks_between_recurrences
    • RecurrencePeriod recurrence_period # belongs_to :recurrence, :polymorphic=>true

I use polymorphic associations here, because I think they might be useful if you want more than one type of recurrence, such both WeeklyRecurrence and DailyRecurrence. But I'm not sure that they're the correct way to model that, so if they turn out not to be, just use has_one :weekly_recurrence and belongs_to :recurrence_period instead.

The Ice cube library seems like it might be useful for calculating recurrences. If WeeklyRecurrence above isn't powerful enough, you might just want to store an Ice cube Schedule object in a model, replacing WeeklyRecurrence. To store a Schedule object in a model, save it as an attribute "schedule", put serialize :schedule in the model definition, and generate a text column "schedule" in the database.

OccurrenceOverride handles the case of a single instance of a recurring event being edited.

  • OccurrenceOverride
    • RecurrencePeriod recurrence_period_to_override # belongs_to :recurrence_period_to_override, :class_name=>'RecurrencePeriod'
    • Time original_start_time # uniquely identifies which recurrence within that RecurrencePeriod to replace
    • Event replacement_event # has_one :replacement_event, :class_name=>'Event'; may be nil, if that recurrence was deleted instead of edited

Instead of storing each occurrence of an event individually, generate them temporarily when you need to show them in the view. In RecurrencePeriod, create a method generate_events_in_range(start_date, end_date) that generates Events, not to save in the database, but just to pass to the view so it can show them.

When a user edits a recurrence, they should have the option to modify all occurrences, all future occurrences, or just that event. If they modify all occurrences, modify the RecurrencePeriod's base_event. If they modify all future occurrences, use a method you should implement on RecurrencePeriod that splits itself into two RecurrencePeriods on either side of a certain date, and then save the changes to just the second period. If they modify only that event, create an OccurrenceOverride for the time that they are overriding, and save the changes to the override's replacement_event.

When a user says a certain event should now recur every two weeks for the foreseeable future, you should create a new RecurrencePeriod with that event as base_event and a nil end_date. Its recurrence should be a new WeeklyRecurrence with weeks_between_recurrence=2, and it should have no OccurrenceOverrides.

Zillah answered 22/4, 2012 at 8:36 Comment(2)
Any problems from experience with not having persisted an occurrence though not being able to access it by id? (performance, complexity, etc.)Cankerous
@Cankerous Good question. I’ve never implemented this, so I’m afraid I don’t know if generating occurrences as needed causes any problems. I suppose you could set up a cache for generated occurrences if necessary. A caching system might use the fact that a generated occurrence to save is conceptually very similar to an OccurrenceOverride, though not identical.Kaddish
T
9

In my case I did something like this :

# Holds most of my event's data; name, description, price ...
class Event < ActiveRecord::Base
  has_many :schedules
  has_many :occurrences
  attr_accessible :started_at, :expired_at # expired_at is optional
end

# Holds my schedule object
class Schedule < ActiveRecord::Base
  belongs_to :event
  attr_accessible :ice_cube_rule # which returns my deserialized ice_cube object
end

# Holds generated or manually created event occurrences 
class Occurrence < ActiveRecord::Base
  belongs_to :event
  attr_accessible :started_at, :expired_at
  attr_accessible :generated # helps me tell which occurrences are out of an ice_cube generated serie
  attr_accessible :canceled_at
end

From there, I used ice_cube to manage the occurrences calculation and stored the results in the occurrences table. I first tried to work without the Occurrence model, but no matter how advanced the rule engine, you'll always have exceptions, so storing the occurrences in their own model gives you flexibility.

Having an Occurrence model makes it a lot easier to display the events on a calendar or with date search filters as you just need to query for occurrences and then display the related event's data instead of gathering all the events in a given date range and then having to filter out the events where the schedule(s) don't match.

Also you can flag an event occurrence as canceled or modify it (setting the generated attribute at false so it does not get cleaned up when editing an ice_cube schedule... or whatever your business need is)

Of course if you have events that repeat indefinitely, you'll want to limit how far in the future you want those occurrences to be generated and use automated rake tasks to clean up the old ones and generate occurrences for the next year or so.

So far this pattern works pretty well for me.

Also, take a look at the recurring_select gem which is a pretty neat ice_cube form input.

Tanika answered 30/1, 2014 at 1:18 Comment(4)
Nice post. Curious on your thoughts with this approach: blog.plataformatec.com.br/2010/04/recurring-events Looks similar to yours.Pikeman
Bruno, the approach you mention is valid as long as you don't need to persist state on these recurrences or deal with exceptions (ie: concert representation pushed to the next day in case of rain)Tanika
@Tanika Do you have a demo app or example app that I can mess with it ?? I like you solutionThermotaxis
@Thermotaxis no I'm sorry, but there's not much more to it than this snippet. The rest is your own business logic. I implemented this technique over a year ago and it still stands.Tanika
D
6

Just an opinion off the top of my head, perhaps comenters will point out a problem I'm not thinking of at the moment:

I would make a RecurringEvent model (or whatever you want to call it) that has_many :events.

Let's say each event is created by an employee (based on your notes), then RecurringEvent would also belong_to :employee. You could then build a has_many :through relationship where an employee has many events and has many recurring events.

The RecurringEvent model could have a start date and a pattern, and it could initially use this pattern to create the individual occuring events. Then on any event that is part of the recurring series you could modify or delete that individual occurance, but you could also 'regenerate the series', deleting all events in the series (or all future events in the series) and rebuilding them based on a new pattern, so for instance move the meeting from "every tuesday" to "every thursday".

One other kind of nice thing about this is you could create an at-a-glance list of recurring events, which might give you some nice insight into people's major obligations.

Like I said, off the top of my head that's how I would approach it, but this is just an idea and I haven't built anything like that so I don't know if there are any big gotchas in the approach I'm suggesting.

Good luck, please post what you end up doing!

Desperate answered 15/4, 2012 at 2:59 Comment(0)
D
-1

I m quite new to Rails, your solution sounds interesting. To create the schedule and associated occurences, do you use conditionnal callbacks in Event model?

In my case, users would be able to create events, weekly recurring or not. So I was thinking about a recurring boolean field in event model. So I guess you would have a first callback to create the schedule:

before_save :create_weekly_schedule, if: :recurring

and basically a second one to create the occurences:

after_save :create_occurences_if_recurring

def create_occurences_if_recurring
  schedules.each do |sched|
    occurences.create(start_date: sched.start_time, end_date: sched.end_time)
  end
end

Does this sound logical with your solution? Thx

Deracinate answered 16/5, 2014 at 23:7 Comment(2)
I see that you were replying to my answer. When you reply to a specific answer, you should post a comment on that answer, or else the author won’t be notified. Also, if your question is long (like this one), don’t put it in another answer on the original question; create a new question on the site that links to the answer you are replying to.Kaddish
As for what I think of your solution, I see a problem with it. You say schedules.each – but if the schedule is simply “repeats every week”, then the each loop will last forever, because the schedule does not specify an ending date. That is why I didn’t generate all the events ahead of time – there are an infinite number of them.Kaddish

© 2022 - 2024 — McMap. All rights reserved.