{"action":"created","comment":{"url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/comments/28179804","id":28179804,"diff_hunk":"@@ -0,0 +1,123 @@\n+---\n+layout: post\n+category :\n+tagline: \"\"\n+tags : [iCal, calendar, ics, Ember, Rails]\n+title: \"iCal subscriptions with Ember and Rails Part 1\"\n+description: \"How to subscribe to your application's events by setting up an .ics endpoint\"\n+author: faizaan_shamsi\n+---\n+{% include JB/setup %}\n+\n+#### Introduction\n+\n+You've built a feature that creates events/schedules/todos. Your users are clamoring to have those events added to their Calendar application. Let's look at how we can do that in the context of a Rails app.\n+\n+#### The iCalendar format\n+\n+We are going to want to create an endpoint in our app that responds with the `.ics` format: [iCalendar](http://en.wikipedia.org/wiki/ICalendar). Most calendar apps such as Apple Calendar and Google Calendar can import an `.ics` file or subscribe to an `.ics` URL. The important thing to note is that we will be returning a VCALENDAR with multiple objects (most likely `VEVENT` or `VTODO`) inside it. You'll have to choose the object that is correct for your use case by looking at the required and optional attributes that best match. We'll be using `VEVENT`.\n+\n+#### Builing iCalendar objects in Ruby\n+\n+Let us assume we have a `Meeting` class in Rails as follows:\n+\n+{% highlight ruby %}\n+\n+class Meeting < ActiveRecord::Base\n+  validates :start_time,  presence: true\n+  validates :duration,    presence: true\n+  validates :description, presence: true\n+  validates :location,    presence: true\n+end\n+\n+{% endhighlight %}\n+\n+We have meeting objects that we would like to turn into `VEVENT` objects. We're going to use the [icalendar](https://github.com/icalendar/icalendar) gem to help us. The important thing to note is that based on the type of object (`VEVENT`, `VTODO`, etc.) there are different required and optional parameters. We can view these by inspecting the source for the [`Icalendar::Event`](https://github.com/icalendar/icalendar/blob/master/lib/icalendar/event.rb) we will be creating. The main things to note:\n+\n+* The `dstamp` and `uid` are required, but we don't need to provide arguments\n+* We must provide a `dtstart`, and may provide a `dtend` or `duration` but not both.\n+* There are additional optional properties that can be added as makes sense.\n+\n+Let's define a `to_ics` method on our `Meeting` class:\n+\n+{% highlight ruby %}\n+\n+class Meeting < ActiveRecord::Base\n+  validates :start_time,  presence: true\n+  validates :duration,    presence: true\n+  validates :description, presence: true\n+  validates :location,    presence: true\n+\n+  def to_ics\n+    event = Icalendar::Event.new\n+    event.dtstart       = self.start_time\n+    event.duration      = self.duration\n+    event.description   = self.description\n+    event.created       = self.created_at\n+    event.last_modified = self.updated_at\n+    event.uid           = self.id # Don't do this in real life\n+    event\n+  end\n+end\n+\n+{% endhighlight %}\n+\n+In our case we've set the uid of the Event based on our meeting's id. We might not want to expose that id publicly so may have to devise some other method to generate a uid.\n+\n+#### Building an iCalendar endpoint in Rails\n+\n+Now that we've seen how we can turn our objects into the `VEVENT` format, we need to create an `.ics` rails endpoint. We want something like this:\n+\n+{% highlight ruby %}\n+\n+  class API::MeetingsController < API::BaseController\n+    # 1\n+    skip_before_action :some_kind_of_auth, only: [:index]\n+    # 2\n+    include ActionController::MimeResponds\n+\n+    def index\n+      meetings = Meeting.all\n+      # 3\n+      calendar = Icalendar::Calendar.new\n+\n+      meetings.each do |meeting|\n+        # 4\n+        calendar.add_event(meeting.to_ics)\n+      end\n+\n+      respond_to do |format|\n+        # 5\n+        format.ics { render text: calendar.to_ical }\n+      end\n+    end\n+  end\n+\n+{% endhighlight %}\n+\n+This is a simplified case, you may have a somewhat different controller and we'll assume the route has already been created. Let's see what's happening on each of these lines:\n+\n+1. Since this URL will be exposed to an external app we will disable any auth that would have otherwise inherited. In a future post we'll talk about how to make this more secure\n+\n+2. We need to allow our Rails app to respond with a `text/calendar` [MIME Type](http://en.wikipedia.org/wiki/MIME). Rails fortunately has support for this.\n+\n+3. We use the `icalendar` gem to create a calendar that can be converted to a `VCALENDAR`","path":"_posts/2015-04-10-iCal-subscriptions-with-ember-and-rails.md","position":104,"original_position":104,"commit_id":"96dfc3f161c636a98818a37b6945815b3c7872e5","original_commit_id":"96dfc3f161c636a98818a37b6945815b3c7872e5","user":{"login":"HeroicEric","id":602204,"avatar_url":"https://avatars.githubusercontent.com/u/602204?v=3","gravatar_id":"","url":"https://api.github.com/users/HeroicEric","html_url":"https://github.com/HeroicEric","followers_url":"https://api.github.com/users/HeroicEric/followers","following_url":"https://api.github.com/users/HeroicEric/following{/other_user}","gists_url":"https://api.github.com/users/HeroicEric/gists{/gist_id}","starred_url":"https://api.github.com/users/HeroicEric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/HeroicEric/subscriptions","organizations_url":"https://api.github.com/users/HeroicEric/orgs","repos_url":"https://api.github.com/users/HeroicEric/repos","events_url":"https://api.github.com/users/HeroicEric/events{/privacy}","received_events_url":"https://api.github.com/users/HeroicEric/received_events","type":"User","site_admin":false},"body":"\"that can **later** be converted to a ...\r\n\r\nLess likely to get confused","created_at":"2015-04-10T20:56:56Z","updated_at":"2015-04-10T20:56:56Z","html_url":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8#discussion_r28179804","pull_request_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8","_links":{"self":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/comments/28179804"},"html":{"href":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8#discussion_r28179804"},"pull_request":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8"}}},"pull_request":{"url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8","id":33074132,"html_url":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8","diff_url":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8.diff","patch_url":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8.patch","issue_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/8","number":8,"state":"open","locked":false,"title":"WIP FS ical integration","user":{"login":"faizaanshamsi","id":3848451,"avatar_url":"https://avatars.githubusercontent.com/u/3848451?v=3","gravatar_id":"","url":"https://api.github.com/users/faizaanshamsi","html_url":"https://github.com/faizaanshamsi","followers_url":"https://api.github.com/users/faizaanshamsi/followers","following_url":"https://api.github.com/users/faizaanshamsi/following{/other_user}","gists_url":"https://api.github.com/users/faizaanshamsi/gists{/gist_id}","starred_url":"https://api.github.com/users/faizaanshamsi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/faizaanshamsi/subscriptions","organizations_url":"https://api.github.com/users/faizaanshamsi/orgs","repos_url":"https://api.github.com/users/faizaanshamsi/repos","events_url":"https://api.github.com/users/faizaanshamsi/events{/privacy}","received_events_url":"https://api.github.com/users/faizaanshamsi/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-04-10T19:02:53Z","updated_at":"2015-04-10T20:56:56Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2fc8138ad3f19f99ef34d1e826dcdd0bec876a47","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8/commits","review_comments_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8/comments","review_comment_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/comments{/number}","comments_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/8/comments","statuses_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/statuses/96dfc3f161c636a98818a37b6945815b3c7872e5","head":{"label":"greenfieldhq:fs-ical-integration","ref":"fs-ical-integration","sha":"96dfc3f161c636a98818a37b6945815b3c7872e5","user":{"login":"greenfieldhq","id":7207275,"avatar_url":"https://avatars.githubusercontent.com/u/7207275?v=3","gravatar_id":"","url":"https://api.github.com/users/greenfieldhq","html_url":"https://github.com/greenfieldhq","followers_url":"https://api.github.com/users/greenfieldhq/followers","following_url":"https://api.github.com/users/greenfieldhq/following{/other_user}","gists_url":"https://api.github.com/users/greenfieldhq/gists{/gist_id}","starred_url":"https://api.github.com/users/greenfieldhq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/greenfieldhq/subscriptions","organizations_url":"https://api.github.com/users/greenfieldhq/orgs","repos_url":"https://api.github.com/users/greenfieldhq/repos","events_url":"https://api.github.com/users/greenfieldhq/events{/privacy}","received_events_url":"https://api.github.com/users/greenfieldhq/received_events","type":"Organization","site_admin":false},"repo":{"id":18532488,"name":"greenfieldhq.github.io","full_name":"greenfieldhq/greenfieldhq.github.io","owner":{"login":"greenfieldhq","id":7207275,"avatar_url":"https://avatars.githubusercontent.com/u/7207275?v=3","gravatar_id":"","url":"https://api.github.com/users/greenfieldhq","html_url":"https://github.com/greenfieldhq","followers_url":"https://api.github.com/users/greenfieldhq/followers","following_url":"https://api.github.com/users/greenfieldhq/following{/other_user}","gists_url":"https://api.github.com/users/greenfieldhq/gists{/gist_id}","starred_url":"https://api.github.com/users/greenfieldhq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/greenfieldhq/subscriptions","organizations_url":"https://api.github.com/users/greenfieldhq/orgs","repos_url":"https://api.github.com/users/greenfieldhq/repos","events_url":"https://api.github.com/users/greenfieldhq/events{/privacy}","received_events_url":"https://api.github.com/users/greenfieldhq/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/greenfieldhq/greenfieldhq.github.io","description":"","fork":false,"url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io","forks_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/forks","keys_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/teams","hooks_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/hooks","issue_events_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/events{/number}","events_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/events","assignees_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/assignees{/user}","branches_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/branches{/branch}","tags_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/tags","blobs_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/statuses/{sha}","languages_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/languages","stargazers_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/stargazers","contributors_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/contributors","subscribers_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/subscribers","subscription_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/subscription","commits_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/contents/{+path}","compare_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/merges","archive_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/downloads","issues_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues{/number}","pulls_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls{/number}","milestones_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/milestones{/number}","notifications_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/labels{/name}","releases_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/releases{/id}","created_at":"2014-04-07T20:10:35Z","updated_at":"2015-03-25T19:55:29Z","pushed_at":"2015-04-10T20:37:12Z","git_url":"git://github.com/greenfieldhq/greenfieldhq.github.io.git","ssh_url":"git@github.com:greenfieldhq/greenfieldhq.github.io.git","clone_url":"https://github.com/greenfieldhq/greenfieldhq.github.io.git","svn_url":"https://github.com/greenfieldhq/greenfieldhq.github.io","homepage":null,"size":1584,"stargazers_count":0,"watchers_count":0,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"greenfieldhq:master","ref":"master","sha":"85d4ad177c88f6d68f43c2b218e9185e4f0ee684","user":{"login":"greenfieldhq","id":7207275,"avatar_url":"https://avatars.githubusercontent.com/u/7207275?v=3","gravatar_id":"","url":"https://api.github.com/users/greenfieldhq","html_url":"https://github.com/greenfieldhq","followers_url":"https://api.github.com/users/greenfieldhq/followers","following_url":"https://api.github.com/users/greenfieldhq/following{/other_user}","gists_url":"https://api.github.com/users/greenfieldhq/gists{/gist_id}","starred_url":"https://api.github.com/users/greenfieldhq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/greenfieldhq/subscriptions","organizations_url":"https://api.github.com/users/greenfieldhq/orgs","repos_url":"https://api.github.com/users/greenfieldhq/repos","events_url":"https://api.github.com/users/greenfieldhq/events{/privacy}","received_events_url":"https://api.github.com/users/greenfieldhq/received_events","type":"Organization","site_admin":false},"repo":{"id":18532488,"name":"greenfieldhq.github.io","full_name":"greenfieldhq/greenfieldhq.github.io","owner":{"login":"greenfieldhq","id":7207275,"avatar_url":"https://avatars.githubusercontent.com/u/7207275?v=3","gravatar_id":"","url":"https://api.github.com/users/greenfieldhq","html_url":"https://github.com/greenfieldhq","followers_url":"https://api.github.com/users/greenfieldhq/followers","following_url":"https://api.github.com/users/greenfieldhq/following{/other_user}","gists_url":"https://api.github.com/users/greenfieldhq/gists{/gist_id}","starred_url":"https://api.github.com/users/greenfieldhq/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/greenfieldhq/subscriptions","organizations_url":"https://api.github.com/users/greenfieldhq/orgs","repos_url":"https://api.github.com/users/greenfieldhq/repos","events_url":"https://api.github.com/users/greenfieldhq/events{/privacy}","received_events_url":"https://api.github.com/users/greenfieldhq/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/greenfieldhq/greenfieldhq.github.io","description":"","fork":false,"url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io","forks_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/forks","keys_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/teams","hooks_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/hooks","issue_events_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/events{/number}","events_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/events","assignees_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/assignees{/user}","branches_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/branches{/branch}","tags_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/tags","blobs_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/statuses/{sha}","languages_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/languages","stargazers_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/stargazers","contributors_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/contributors","subscribers_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/subscribers","subscription_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/subscription","commits_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/contents/{+path}","compare_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/merges","archive_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/downloads","issues_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues{/number}","pulls_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls{/number}","milestones_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/milestones{/number}","notifications_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/labels{/name}","releases_url":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/releases{/id}","created_at":"2014-04-07T20:10:35Z","updated_at":"2015-03-25T19:55:29Z","pushed_at":"2015-04-10T20:37:12Z","git_url":"git://github.com/greenfieldhq/greenfieldhq.github.io.git","ssh_url":"git@github.com:greenfieldhq/greenfieldhq.github.io.git","clone_url":"https://github.com/greenfieldhq/greenfieldhq.github.io.git","svn_url":"https://github.com/greenfieldhq/greenfieldhq.github.io","homepage":null,"size":1584,"stargazers_count":0,"watchers_count":0,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8"},"html":{"href":"https://github.com/greenfieldhq/greenfieldhq.github.io/pull/8"},"issue":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/8"},"comments":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/issues/8/comments"},"review_comments":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8/comments"},"review_comment":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/pulls/8/commits"},"statuses":{"href":"https://api.github.com/repos/greenfieldhq/greenfieldhq.github.io/statuses/96dfc3f161c636a98818a37b6945815b3c7872e5"}}}}