Skip to main content

24 Recurring events with the ice_cube gem

Episode 159 · November 28, 2016

See how to take rules from the ice_cube gem and add recurring events to any calendar

Gems


Transcripts

What's up guys? This episode we're talking about how to add recurring events into the simple calendar UI and how to create them, how to save them in the database and how to display them. Now this feature seems simple as a human being, right? You have an event, you want to tell it to recurr every single Tuesday and so when you display a calendar it should just display one every Tuesday right? Well, it's not really that simple, what we need to do is we need to figure out how to store the rules in the database, how to pull the rules out, how to calculate those rules based on the days that we want to display on the calendar and then how to take those events and put them in the calendar, so there's a lot to it, and that's not to mention all the exceptions that you might want to add to those, so maybe you do a every Monday meeting, but then there's a holiday and you want to delete that meeting for that one day, not the entire recurring thing. You just want to put an exception for the one day, what do you do? Well these two episodes we're going to cover that, we're going to be using the ice_cube gem. This let's you define rules for the recurring events and we'll use that in order to generate events for the calendar. We're also going to use recurring_select. This gem allows us to create a select box where we can say: Set the schedule and it pops up a little modal, and we can choose that we want a weekly event from Monday and Friday every single week, hit OK and that will save it into our database. There's a lot going on here so let's just get started with the code. Let's just create a new rails app rails new recurring_events and we can cd into recurring events and open up our Gemfile and let's go to the bottom and say gem 'simple_calendar' and gem 'recurring_select' This is going to give us that select box with the modal, but we need to add a GitHub recurring select fork so it will be compatible with rails 5. Rundown app has forked it and made it compatible with rails 5, so we'll go ahead and use that version. We can run bundle to install this, and that will give us both the calendar and recurring select depends on the ice_cube gem so we automatically have that installed just by adding recurring_select Let's create our model for this. Generate a scaffold named event. Each event is going to have a name, they'll also have a start time, so this will be for the one-time events so we can add those to specific days and times and then we'll have a recurring attributes in the text column and this will be a serialized hash that we will save into the database and be able to reload and easily load into ice_cube. Now that we've got our model, we can run rake db:migrate to set up our database, and we need to go and modify our form so that we can use that select. rather than a text area we will have:

app/views/events/_form.html.erb

<div class="field">
    <%= f.label :recurring %>
    <%= f.select_recurring :recurring %>
</div>

To add the recurring drop down and the modal for that, but we can also pass a nil and allow blank as true, and this is going to give us the ability for us to clear out a schedule that was already added, so if we want to edit we could actually make it a one-time event instead. We'll add that and we can load this up in our rails server and check it out. Our application is going to have both those one time events, so I'll say: One time event, and we'll create that for right now, and then we'll also have our event for recurring event and this should set the schedule. This doesn't quite work because we need to add our application.js to //= require recurring_select and we also need in our application.css to add *= require recurring_select now if we refresh this, and we try our set schedule we will get the JavaScript and the CSS for it and we can choose our days and recurring events will be saved. This time you'll see that it saves some data into the recurring column, and the other weirdly enough saved the string of null and that's because it's serializing this JSON and it's sending it over and saving it in that column and we need to actually be able to parse this out rather than using the JSON we want to just save this in our own serialized column. So what we'll do is we'll override the setter for the recurring column, and we will parse that out into a hash if it's a valid recurring rule, and if it's not, we'll just save nil to it so we don't have the string of nul, we'll actually have nil in the database. Now we'll keep it consistent for us to query against. Let's open up the event model and let's serialize the recurring column to a hash.

app/models/event.rb

class Event < ApplicationRecord 
    serialize :recurring, Hash 

    def recurring=(value)
        if RecurringSelect.is_valid_rule?(value)
            super(RecurringSelect.dirty_hash_to_rule(value).to_hash)
        else
            super(nil)
        end
    end
end

If we save that and try this out in the browser, we can now create a one time event without the recurring stuff, and you'll see there's an actual empty ruby hash here instead of the string of nul, and if we create a recurring event and we set this to: Weekly every Monday we can create that event and you see symbols here instead of strings, so this is definitely ruby format instead of JSON format. So that's a ruby hash and that means that our conversions have been working and now we can go work on taking those rules and converting them into events so that we can display them in the calendar. The easiest place to start is probably to add that calendar in and get the one-time events added to the calendar so let's do that. Let's go into our index and let's say

app/views/events/index.html.erb

<%= month_calendar events: @events do |day, events| %>
    <div><strong><%= day.to_s(:long) %></strong></div>
    <% events.each do |event| %> 
    <div>
        <%= link_to event.name, event %>
    </div>
    <% end %> 
<% end %> 

This should print out our events. Now this doesn't actually know the difference between our one-time events and our recurring events, so we need to figure out how we want to handle that. The way I'm going to handle this is we're going to create a new array called calendar_events and this will include all the one-time events plus all of the generated events for the recurring ones. We can go into our

app/controllers/events_controller.rb

def index 
    @events = Event.all 
    @calendar_events = @events.flat_map{ |e| e.calendar_events(params.fetch(:start_date, Time.zone.now).to_date ) }
end

We have our fetch, we call that method, and now we need to create that method in our event model. Here is where we add our calendar_events method and it will take that start date and we need to figure out how to populate this. First thing is first, if it's a one time event we want to just return that one event, but if it's multiple events we want to return an array for that recurring one that might be daily or weekly or whatever the case, so if the recurring rules exist, we want to use those in Ice Cube to generate that array for our time frame:

app/models/event.rb

def rule
    IceCube::Rule.from_hash recurring 
end 

def schedule(start)
    schedule = IceCube::Schedule.new(start)
    schedule.add_recurrence_rule(rule)
    schedule
end 

def calendar_events(start)
    if recurring.empty? 
        [self]
    else
        start_date = start.beginning_of_month.beginning_of_week 
        end_date = start.end_of_month.end_of_week 
        schedule(start_date).occurrences(end_date).map do |date|
            Event.new(id: id, name: name, start_time: date)
        end
    end
end

This schedule is going to allow us to add in all those rules that might be necessary for example, we might have some exceptions and so if we put those exceptions in here as well as part of our call but we don't have those yet, we'll talk about those in the future, and for now we can do this. Now that we've got our schedule we can use it to generate that array of events. We grab the beginning of the week for our calendar view so if you're doing a week view or something like that, your calculation here would be a little bit different because you would need to calculate maybe just the beginning of the week not the beginning of the month. You can change that accordingly and we'll have our end date and this is similar. We'll have that and be able to call our schedule and our occurrences will just give us an array of days of times back and we can give it an end date so that it knows how many to calculate, we can go through each of these here and grab the date into a variable and create our new events, We can create our new events, we'll give it the same id as the one in the database, so this is just in memory. We're telling it to create a duplicate id, but that's just because we're going to create this in memory and display this in the calendar and so it will always link back to the same event, so rather than creating these in the database, we're going to create it in memory, but when you generate that link_to it's always going to point to the exact same event. This is our work around in order to get those to display and interact with just as if there were regular old records even though they're not, so we'll assign it the same name and you would want to assign all the other attributes that you might like in here, and we'll assign the start time to this to be that date.

One customization that you might want to make is that if you want to put a specific time on here, you can go ahead and modify this date to take the start time out of the event. So if you wanted the user to create a record that happened at nine am every Monday, you can go ahead and take the original 9am and assign it to your new one in memory, and that will go ahead and allow us to create our events.

Refreshing our page we get to see that all of those recurring events are being displayed on those days so our recurring event for the Monday rule is generating new recurring event lines in every day and a one-time event displays in the same correct place as well. That's pretty cool. It looks like our links there are not correctly working so let's go into the event's index and let's try changing this <%= link_to event.name, event_path(event.id) %> We'll explicitly set that id and because it wasn't persisted that id was not being added, so there we want to add event.id because this is not a permanent record in the database, so the link_to said: Let's not link it anywhere because it hasn't been saved yet, so by expressing this specifically, we can go create a link to that recurring event. Each of these recurring events will always point to event /9 and that is coming from those records that aren't actually saved, so this recurring event record here that is number nine is that actually really in the view, it's actually just the ones that are just created from our calendar array events, so this does correctly set those up. If we wanted to create a new one that was maybe weekly every Tuesday Thursday we can add that, and now we have it automatically on every Tuesday and Thursday, and if we change months of the calendar we'll see that his actually includes those as well and our one time event only shows up in November because that is the day that it was supposed to be.

This goes without saying, but your recurring events can get massively more complex if you want to go add a bunch more features to this. I've implemented the bare minimum that you need and one of the flaws with this is that what happens is that your events happen all the way into the past and all the way into the future infinitely. So our event, even though we created it November it actually goes back into the past as long as you can remember. It's not likely that you want to do that with your views like that, you generally want to say: When I create it any time in the future we want to display it but not everything in the past, and that's what Google Calendar does. There's a lot of other tweaks you can make like changing the schedule start to do the one time that you typed in when you created that record, and changing that will allow us to say: Well it starts November 14 and it happens anytime into the future and not into the past. That's nice for you to be able to customize and you can go as deep into this as you would like, so take a look at ice_cubes schedule and it's rules and how to query for those, and learn about that because once you've got the rules in there you can query this and make changes to it in order to get the output that you want in your views, so this loop is really simple, you just give it a start and an end and it will generate the occurrences between those days.

There are a ton of changes you probably want to make to this for your specific application, you might want to edit your controller so it queries for all the one-time events in that window, and then it finds all the recurring events that will be in that window as well so those are going to be important for doing a little bit of efficiency and only loading the events you need for the calendar view on that month or week or whatever it is. It's up to you to make some of those tweaks but this is a really good foundation to get you started in all of that and I will be back next week with an addition to this to talk about adding exceptions so we can have recurring events into the future but we remove a holiday and so we don't do a meeting on the holiday. We'll talk about that in the next episode, till then. I will talk to you later. Peace ✌️

Transcript written by Miguel

Discussion