Resources

Be sure to checkout the previous episodes as they cover much of the prerequisite information.

Episode 42 - Full Calendar Events and Scheduling
Episode 93 - Recurring Events with ice_cube

Source - https://github.com/driftingruby/094-recurring-events-on-full-calendar

Summary

# Gemfile
source 'https://rails-assets.org' do
  gem 'rails-assets-fullcalendar'
  gem 'rails-assets-momentjs'
end

gem 'ice_cube'

# recurring_events_migration.rb
# rails g model recurring_event title anchor:date frequency:integer color

class CreateRecurringEvents < ActiveRecord::Migration[5.0]
  def change
    create_table :recurring_events do |t|
      t.string :title
      t.date :anchor
      t.integer :frequency, limit: 1, default: 0
      t.string :color

      t.timestamps
    end
  end
end

# recurring_events_controller.rb
class RecurringEventsController < ApplicationController
  before_action :set_recurring_event, only: [:show, :edit, :update, :destroy]

  def index
    @recurring_events = RecurringEvent.all
  end

  def show
  end

  def new
    @recurring_event = RecurringEvent.new
  end

  def edit
  end

  def create
    @recurring_event = RecurringEvent.new(recurring_event_params)
    @recurring_event.save
  end

  def update
    if params[:event]
      @recurring_event.update(anchor: params[:event][:start])
    else
      @recurring_event.update(recurring_event_params)
    end
  end

  def destroy
    @recurring_event.destroy
  end

  private
  
  def set_recurring_event
    @recurring_event = RecurringEvent.find(params[:id])
  end

  def recurring_event_params
    params.require(:recurring_event).permit(:title, :anchor, :frequency, :color)
  end
end

# recurring_event.rb
class RecurringEvent < ApplicationRecord
  enum frequency: { weekly: 0, biweekly: 1, monthly: 2, annually: 3 }

  validates :anchor, presence: true
  validates :frequency, presence: true

  def schedule
    @schedule ||= begin
      schedule = IceCube::Schedule.new(now = anchor)
      case frequency      
      when 'weekly'
        schedule.add_recurrence_rule IceCube::Rule.weekly(1)
      when 'biweekly'
        schedule.add_recurrence_rule IceCube::Rule.weekly(2)
      when 'monthly'
        schedule.add_recurrence_rule IceCube::Rule.monthly(1)
      when 'annually'
        schedule.add_recurrence_rule IceCube::Rule.yearly(1)
      end
      schedule
    end
  end

  def events(start_date, end_date)
    start_frequency = start_date ? start_date.to_date : Date.today - 1.year
    end_frequency = end_date ? end_date.to_date : Date.today + 1.year
    schedule.occurrences_between(start_frequency, end_frequency)
  end

end

# full_calendar.js
var initialize_calendar;
initialize_calendar = function() {
  $('.calendar').each(function(){
    var calendar = $(this);
    calendar.fullCalendar({
      header: {
        left: 'prev,next today',
        center: 'title',
        right: 'month,agendaWeek,agendaDay'
      },
      selectable: true,
      selectHelper: true,
      editable: true,
      eventLimit: true,
      eventSources: [
        '/events.json',
        '/recurring_events.json'
      ],
      select: function(start, end) {
        $.getScript('/events/new', function() {
          $('#event_date_range').val(moment(start).format("MM/DD/YYYY HH:mm") + ' - ' + moment(end).format("MM/DD/YYYY HH:mm"))
          date_range_picker();
          $('.start_hidden').val(moment(start).format('YYYY-MM-DD HH:mm'));
          $('.end_hidden').val(moment(end).format('YYYY-MM-DD HH:mm'));
        });

        calendar.fullCalendar('unselect');
      },

      eventDrop: function(event, delta, revertFunc) {
        event_data = { 
          event: {
            id: event.id,
            start: event.start.format(),
            end: event.end.format()
          }
        };
        $.ajax({
            url: event.update_url,
            data: event_data,
            type: 'PATCH'
        });
      },
      
      eventClick: function(event, jsEvent, view) {
        $.getScript(event.edit_url, function() {
          $('#event_date_range').val(moment(event.start).format("MM/DD/YYYY HH:mm") + ' - ' + moment(event.end).format("MM/DD/YYYY HH:mm"))
          date_range_picker();
          $('.start_hidden').val(moment(event.start).format('YYYY-MM-DD HH:mm'));
          $('.end_hidden').val(moment(event.end).format('YYYY-MM-DD HH:mm'));
        });
      }
    });
  })
};
$(document).on('turbolinks:load', initialize_calendar);

# index.json.jbuilder
json.partial! @recurring_events, 
              partial: 'recurring_events/recurring_event', 
              as: :recurring_event

# _recurring_event.json.jbuilder
events = recurring_event.events(params[:start], params[:end])
json.array! events do |event|
  json.id "recurring_#{recurring_event.id}"
  json.title recurring_event.title
  json.start event.strftime('%Y-%m-%d')
  json.end (event + 1.day).strftime('%Y-%m-%d')
  json.color recurring_event.color unless recurring_event.color.blank?
  json.allDay true
  json.update_url recurring_event_path(recurring_event, method: :patch)
  json.edit_url edit_recurring_event_path(recurring_event)
end

# update.js.erb
$('.calendar').fullCalendar('removeEvents', '<%= "recurring_#{@recurring_event.id}" %>');
$('.calendar').fullCalendar(
  'renderEvents', 
  $.parseJSON("<%= j render(@recurring_event, format: :json).html_safe %>"), 
  true
);
$('.modal').modal('hide');