Previously I wrote the Complete guide to iCalendar events with Ruby that focused on creating events that can be exported to a calendar in the .ics format.

Recently Hey Calendar added a feature to import events to their calendar.

icalendar-hey-import-ics

This got me intrigued on how I can parse an .ics file and import it into my app.

Obviously, it can be done with the same gem icalendar, specifically with the .parse method.

Create a parser #

First, install icalendar gem:

bundle add icalendar

Next, create a method to receive a .ics file, parse it and create an event:

# app/models/event.rb

# require 'icalendar'
class Event < ApplicationRecord

  def self.create_from_ics(file)
    cal_file = File.open(file)
    cal = Icalendar::Calendar.parse(cal_file).first
    cal_event = cal.events.first
    Event.create(
      name: cal_event.summary.strip,
      description: cal_event.description.strip,
      starts_at: cal_event.dtstart,
      ends_at: cal_event.dtend,
      location: cal_event.location.strip
    )
  end
end

Here’s a dummy .ics event you can play with!

Create a form #

route

# config/routes.rb
Rails.application.routes.draw do
  resources :events
+  post "events/import" => "events#import"
end

view

# app/views/events/index.html.erb
<%= form_with url: events_import_path, method: :post do |form| %>
  <%#= form.file_field :file %>
  <%= form.file_field :files, multiple: true %>
  <%= form.submit "Import" %>
<% end %>

controller

# app/controllers/events_controller.rb
class EventsController < ApplicationController
  def import
    files = params[:files]
    files = files.reject(&:blank?)

    files.each do |file|
      Event.create_from_ics(file)
    end

    redirect_to events_url, notice: "Importing event from ICS file"
  end
end

Voila! Now we can upload multiple .ics files and create events from them:

import ics events

Using ActiveJob #

Create a job rails g job IcsImport

# app/jobs/ics_import_job.rb

# gem icalendar
require 'icalendar'

class IcsImportJob < ApplicationJob
  queue_as :default

  def perform(file_path:)
    # file_path = 'test/fixtures/files/Sunday+morning+yoga+in+Antibes+.ics'
    # cal_file = File.open(file_path)

    cal_file = File.open(file_path)
    cal = Icalendar::Calendar.parse(cal_file).first
    cal_event = cal.events.first

    event_object = OpenStruct.new(
      name: cal_event.summary.strip,
      description: cal_event.description.strip,
      starts_at: cal_event.dtstart,
      ends_at: cal_event.dtend,
      location: cal_event.location.strip
    )
    Event.create(event_object.to_h)
  end
end

🚨 It’s tricker with the controller!

If you just call IcsImportJob.perform_later(params[:file]), you will get an error ActionDispatch::Http::UploadedFile.

Copilot to the rescue:

ActionDispatch::Http::UploadedFile

update the events controller accordingly:

# app/controllers/events_controller.rb
class EventsController < ApplicationController
  def import
    files = params[:files]
    files = files.reject(&:blank?)

    # files.each do |file|
    #   Event.create_from_ics(file)
    # end

    files.each do |file|
      tempfile = Tempfile.new
      tempfile.binmode
      tempfile.write(file.read)
      tempfile.close
      IcsImportJob.perform_later(file_path: tempfile.path)
    end

    redirect_to events_url, notice: "Importing event from ICS file"
  end
end

Again, here’s a dummy .ics event you can play with. I suggest storing it in test/fixtures/files/*.

Write some tests

# test/jobs/ics_import_job_test.rb
require "test_helper"

class IcsImportJobTest < ActiveJob::TestCase
  test "imports an event from an ICS file" do
    file_path = "test/fixtures/files/Sunday+morning+yoga+in+Antibes+.ics"
    IcsImportJob.perform_now(file_path:)
    event = Event.last
    assert_equal "Sunday morning yoga in Antibes", event.name
    assert_equal "Plage de la Gravette, Antibes (Small beach behind Port Vauban, Antibes, France)", event.location
    assert_equal "2024-03-03 10:30:00 +0100", event.starts_at
    assert_equal "2024-03-03 11:30:00 +0100", event.ends_at
  end
end

That’s it! Good luck building your calendar app!