From 20dfe25c151cc883ce0d38b67125b5ca41e6d422 Mon Sep 17 00:00:00 2001
From: Imre Farkas <ifarkas@gitlab.com>
Date: Thu, 31 May 2018 14:01:04 +0000
Subject: [PATCH] Export assigned issues in iCalendar feed

---
 Gemfile                                       |  3 +
 Gemfile.lock                                  |  2 +
 app/controllers/concerns/issues_action.rb     | 15 ++++-
 app/controllers/profiles_controller.rb        |  6 +-
 app/controllers/projects/issues_controller.rb | 15 ++++-
 app/finders/issues_finder.rb                  |  6 ++
 app/helpers/calendar_helper.rb                |  8 +++
 app/helpers/rss_helper.rb                     |  2 +-
 app/models/issue.rb                           | 16 +++--
 app/models/user.rb                            |  8 +--
 app/views/dashboard/issues.html.haml          |  3 +-
 app/views/dashboard/issues_calendar.ics.haml  |  1 +
 app/views/groups/issues.html.haml             |  5 +-
 app/views/groups/issues_calendar.ics.haml     |  1 +
 app/views/issues/_issues_calendar.ics.ruby    | 15 +++++
 .../personal_access_tokens/index.html.haml    | 14 ++--
 app/views/projects/issues/_nav_btns.html.haml |  4 +-
 app/views/projects/issues/calendar.ics.haml   |  1 +
 app/views/shared/icons/_icon_calendar.svg     |  1 +
 .../shared/issuable/_feed_buttons.html.haml   |  4 ++
 .../unreleased/44184-issues_ical_feed.yml     |  5 ++
 config/routes/dashboard.rb                    |  1 +
 config/routes/group.rb                        |  3 +-
 config/routes/profile.rb                      |  2 +-
 config/routes/project.rb                      |  1 +
 ...54_rename_users_rss_token_to_feed_token.rb | 15 +++++
 ...08143355_cleanup_users_rss_token_rename.rb | 13 ++++
 db/schema.rb                                  |  4 +-
 doc/user/project/issues/due_dates.md          |  8 +++
 lib/gitlab/auth/request_authenticator.rb      |  2 +-
 lib/gitlab/auth/user_auth_finders.rb          | 18 +++--
 lib/support/nginx/gitlab                      | 16 ++---
 lib/support/nginx/gitlab-ssl                  | 16 ++---
 lib/tasks/tokens.rake                         | 10 +--
 .../application_controller_spec.rb            | 28 +++++---
 spec/factories/users.rb                       |  4 --
 spec/features/atom/dashboard_issues_spec.rb   | 12 ++--
 spec/features/atom/dashboard_spec.rb          |  6 +-
 spec/features/atom/issues_spec.rb             | 13 ++--
 spec/features/atom/users_spec.rb              |  6 +-
 spec/features/dashboard/activity_spec.rb      |  4 +-
 spec/features/dashboard/issues_filter_spec.rb |  6 +-
 spec/features/dashboard/issues_spec.rb        |  4 +-
 spec/features/dashboard/projects_spec.rb      |  2 +-
 spec/features/groups/activity_spec.rb         |  8 +--
 spec/features/groups/issues_spec.rb           | 16 +++--
 spec/features/groups/show_spec.rb             |  4 +-
 spec/features/ics/dashboard_issues_spec.rb    | 65 ++++++++++++++++++
 spec/features/ics/group_issues_spec.rb        | 67 +++++++++++++++++++
 spec/features/ics/project_issues_spec.rb      | 66 ++++++++++++++++++
 .../filtered_search/filter_issues_spec.rb     |  4 +-
 spec/features/issues_spec.rb                  | 14 ++++
 spec/features/profile_spec.rb                 | 12 ++--
 spec/features/projects/activity/rss_spec.rb   |  4 +-
 spec/features/projects/commits/rss_spec.rb    |  8 +--
 spec/features/projects/issues/rss_spec.rb     |  8 +--
 spec/features/projects/show/rss_spec.rb       |  4 +-
 spec/features/projects/tree/rss_spec.rb       |  4 +-
 spec/features/users/rss_spec.rb               |  4 +-
 spec/helpers/calendar_helper_spec.rb          | 20 ++++++
 spec/helpers/rss_helper_spec.rb               |  8 +--
 .../gitlab/auth/request_authenticator_spec.rb | 10 +--
 .../lib/gitlab/auth/user_auth_finders_spec.rb | 46 +++++++++----
 .../concerns/token_authenticatable_spec.rb    |  2 +-
 spec/models/user_spec.rb                      | 12 ++--
 spec/requests/rack_attack_global_spec.rb      |  2 +-
 spec/routing/routing_spec.rb                  | 10 ++-
 spec/support/features/rss_shared_examples.rb  | 24 +++----
 spec/tasks/tokens_spec.rb                     |  4 +-
 69 files changed, 569 insertions(+), 186 deletions(-)
 create mode 100644 app/helpers/calendar_helper.rb
 create mode 100644 app/views/dashboard/issues_calendar.ics.haml
 create mode 100644 app/views/groups/issues_calendar.ics.haml
 create mode 100644 app/views/issues/_issues_calendar.ics.ruby
 create mode 100644 app/views/projects/issues/calendar.ics.haml
 create mode 100644 app/views/shared/icons/_icon_calendar.svg
 create mode 100644 app/views/shared/issuable/_feed_buttons.html.haml
 create mode 100644 changelogs/unreleased/44184-issues_ical_feed.yml
 create mode 100644 db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb
 create mode 100644 db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb
 create mode 100644 spec/features/ics/dashboard_issues_spec.rb
 create mode 100644 spec/features/ics/group_issues_spec.rb
 create mode 100644 spec/features/ics/project_issues_spec.rb
 create mode 100644 spec/helpers/calendar_helper_spec.rb

diff --git a/Gemfile b/Gemfile
index 558584202425d..68c7b3dcb08ff 100644
--- a/Gemfile
+++ b/Gemfile
@@ -144,6 +144,9 @@ gem 'truncato', '~> 0.7.9'
 gem 'bootstrap_form', '~> 2.7.0'
 gem 'nokogiri', '~> 1.8.2'
 
+# Calendar rendering
+gem 'icalendar'
+
 # Diffs
 gem 'diffy', '~> 3.1.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index fe20570ae894f..4d2bd62bec03f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -410,6 +410,7 @@ GEM
     httpclient (2.8.3)
     i18n (0.9.5)
       concurrent-ruby (~> 1.0)
+    icalendar (2.4.1)
     ice_nine (0.11.2)
     influxdb (0.2.3)
       cause
@@ -1060,6 +1061,7 @@ DEPENDENCIES
   html-pipeline (~> 2.7.1)
   html2text
   httparty (~> 0.13.3)
+  icalendar
   influxdb (~> 0.2)
   jira-ruby (~> 1.4)
   jquery-atwho-rails (~> 1.3.2)
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 3b11a37336881..b6eb7d292fcbe 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -17,10 +17,23 @@ def issues
   end
   # rubocop:enable Gitlab/ModuleWithInstanceVariables
 
+  # rubocop:disable Gitlab/ModuleWithInstanceVariables
+  def issues_calendar
+    @issues = issuables_collection
+                  .non_archived
+                  .with_due_date
+                  .limit(100)
+
+    respond_to do |format|
+      format.ics { response.headers['Content-Disposition'] = 'inline' }
+    end
+  end
+  # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
   private
 
   def finder_type
     (super if defined?(super)) ||
-      (IssuesFinder if action_name == 'issues')
+      (IssuesFinder if %w(issues issues_calendar).include?(action_name))
   end
 end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 9f5ad23a20f9f..074db36194939 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -34,12 +34,12 @@ def reset_incoming_email_token
     redirect_to profile_personal_access_tokens_path
   end
 
-  def reset_rss_token
+  def reset_feed_token
     Users::UpdateService.new(current_user, user: @user).execute! do |user|
-      user.reset_rss_token!
+      user.reset_feed_token!
     end
 
-    flash[:notice] = "RSS token was successfully reset"
+    flash[:notice] = 'Feed token was successfully reset'
 
     redirect_to profile_personal_access_tokens_path
   end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d69015c866549..35c36c725e24a 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
 
   before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
   before_action :check_issues_available!
-  before_action :issue, except: [:index, :new, :create, :bulk_update]
-  before_action :set_issuables_index, only: [:index]
+  before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
+  before_action :set_issuables_index, only: [:index, :calendar]
 
   # Allow write(create) issue
   before_action :authorize_create_issue!, only: [:new, :create]
@@ -39,6 +39,17 @@ def index
     end
   end
 
+  def calendar
+    @issues = @issuables
+                  .non_archived
+                  .with_due_date
+                  .limit(100)
+
+    respond_to do |format|
+      format.ics { response.headers['Content-Disposition'] = 'inline' }
+    end
+  end
+
   def new
     params[:issue] ||= ActionController::Parameters.new(
       assignee_ids: ""
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 1787b4899cd7b..3626670d1419b 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -75,6 +75,8 @@ def by_due_date(items)
         items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
       elsif filter_by_due_this_month?
         items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
+      elsif filter_by_due_next_month_and_previous_two_weeks?
+        items = items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month)
       end
     end
 
@@ -97,6 +99,10 @@ def filter_by_due_this_month?
     due_date? && params[:due_date] == Issue::DueThisMonth.name
   end
 
+  def filter_by_due_next_month_and_previous_two_weeks?
+    due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name
+  end
+
   def due_date?
     params[:due_date].present?
   end
diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb
new file mode 100644
index 0000000000000..c54b91b0ce5a2
--- /dev/null
+++ b/app/helpers/calendar_helper.rb
@@ -0,0 +1,8 @@
+module CalendarHelper
+  def calendar_url_options
+    { format: :ics,
+      feed_token: current_user.try(:feed_token),
+      due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+      sort: 'closest_future_date' }
+  end
+end
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
index 9ac4df88dc3e1..7d4fa83a67a36 100644
--- a/app/helpers/rss_helper.rb
+++ b/app/helpers/rss_helper.rb
@@ -1,5 +1,5 @@
 module RssHelper
   def rss_url_options
-    { format: :atom, rss_token: current_user.try(:rss_token) }
+    { format: :atom, feed_token: current_user.try(:feed_token) }
   end
 end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 0332bfa937170..559770fa44212 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -14,12 +14,13 @@ class Issue < ActiveRecord::Base
 
   ignore_column :assignee_id, :branch_name, :deleted_at
 
-  DueDateStruct = Struct.new(:title, :name).freeze
-  NoDueDate     = DueDateStruct.new('No Due Date', '0').freeze
-  AnyDueDate    = DueDateStruct.new('Any Due Date', '').freeze
-  Overdue       = DueDateStruct.new('Overdue', 'overdue').freeze
-  DueThisWeek   = DueDateStruct.new('Due This Week', 'week').freeze
-  DueThisMonth  = DueDateStruct.new('Due This Month', 'month').freeze
+  DueDateStruct                   = Struct.new(:title, :name).freeze
+  NoDueDate                       = DueDateStruct.new('No Due Date', '0').freeze
+  AnyDueDate                      = DueDateStruct.new('Any Due Date', '').freeze
+  Overdue                         = DueDateStruct.new('Overdue', 'overdue').freeze
+  DueThisWeek                     = DueDateStruct.new('Due This Week', 'week').freeze
+  DueThisMonth                    = DueDateStruct.new('Due This Month', 'month').freeze
+  DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
 
   belongs_to :project
   belongs_to :moved_to, class_name: 'Issue'
@@ -46,6 +47,7 @@ class Issue < ActiveRecord::Base
   scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
   scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
 
+  scope :with_due_date, -> { where('due_date IS NOT NULL') }
   scope :without_due_date, -> { where(due_date: nil) }
   scope :due_before, ->(date) { where('issues.due_date < ?', date) }
   scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
@@ -53,6 +55,7 @@ class Issue < ActiveRecord::Base
 
   scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
   scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+  scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') }
 
   scope :preload_associations, -> { preload(:labels, project: :namespace) }
 
@@ -119,6 +122,7 @@ def self.project_foreign_key
 
   def self.sort_by_attribute(method, excluded_labels: [])
     case method.to_s
+    when 'closest_future_date' then order_closest_future_date
     when 'due_date'      then order_due_date_asc
     when 'due_date_asc'  then order_due_date_asc
     when 'due_date_desc' then order_due_date_desc
diff --git a/app/models/user.rb b/app/models/user.rb
index 0a838d34054ac..e219ab800ad6b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,7 +26,7 @@ class User < ActiveRecord::Base
   ignore_column :authentication_token
 
   add_authentication_token_field :incoming_email_token
-  add_authentication_token_field :rss_token
+  add_authentication_token_field :feed_token
 
   default_value_for :admin, false
   default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
@@ -1167,11 +1167,11 @@ def update_two_factor_requirement
     save
   end
 
-  # each existing user needs to have an `rss_token`.
+  # each existing user needs to have an `feed_token`.
   # we do this on read since migrating all existing users is not a feasible
   # solution.
-  def rss_token
-    ensure_rss_token!
+  def feed_token
+    ensure_feed_token!
   end
 
   def sync_attribute?(attribute)
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 4bf04dadf01a0..86a21e24ac95e 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -7,8 +7,7 @@
 .top-area
   = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
   .nav-controls
-    = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
-      = icon('rss')
+    = render 'shared/issuable/feed_buttons'
     = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
 
 = render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/issues_calendar.ics.haml b/app/views/dashboard/issues_calendar.ics.haml
new file mode 100644
index 0000000000000..59573e5fecf00
--- /dev/null
+++ b/app/views/dashboard/issues_calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 662db18cf8661..8037cf4b69d54 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,10 +8,7 @@
   .top-area
     = render 'shared/issuable/nav', type: :issues
     .nav-controls
-      = link_to safe_params.merge(rss_url_options), class: 'btn' do
-        = icon('rss')
-        %span.icon-label
-          Subscribe
+      = render 'shared/issuable/feed_buttons'
       = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
 
   = render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/groups/issues_calendar.ics.haml b/app/views/groups/issues_calendar.ics.haml
new file mode 100644
index 0000000000000..59573e5fecf00
--- /dev/null
+++ b/app/views/groups/issues_calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
new file mode 100644
index 0000000000000..3563635d33db2
--- /dev/null
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -0,0 +1,15 @@
+cal = Icalendar::Calendar.new
+cal.prodid = '-//GitLab//NONSGML GitLab//EN'
+cal.x_wr_calname = 'GitLab Issues'
+
+@issues.includes(project: :namespace).each do |issue|
+  cal.event do |event|
+    event.dtstart     = Icalendar::Values::Date.new(issue.due_date)
+    event.summary     = "#{issue.title} (in #{issue.project.full_path})"
+    event.description = "Find out more at #{issue_url(issue)}"
+    event.url         = issue_url(issue)
+    event.transp      = 'TRANSPARENT'
+  end
+end
+
+cal.to_ical
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index d253e8e456e4c..d111113c64653 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -34,18 +34,18 @@
 .row.prepend-top-default
   .col-lg-4.profile-settings-sidebar
     %h4.prepend-top-0
-      RSS token
+      Feed token
     %p
-      Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
+      Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when when your calendar application loads a personalized calendar, and is included in those feed URLs.
     %p
       It cannot be used to access any other data.
-  .col-lg-8.rss-token-reset
-    = label_tag :rss_token, 'RSS token', class: "label-light"
-    = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+  .col-lg-8.feed-token-reset
+    = label_tag :feed_token, 'Feed token', class: "label-light"
+    = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()'
     %p.form-text.text-muted
-      Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
+      Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you.
       You should
-      = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
+      = link_to 'reset it', [:reset, :feed_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS or calendar URLs currently in use will stop working.' }
       if that ever happens.
 
 - if incoming_email_token_enabled?
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 297b928f020ae..0dd2d2e6c5d82 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,5 +1,5 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
-  = icon('rss')
+= render 'shared/issuable/feed_buttons'
+
 - if @can_bulk_update
   = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
 - if show_new_issue_link?(@project)
diff --git a/app/views/projects/issues/calendar.ics.haml b/app/views/projects/issues/calendar.ics.haml
new file mode 100644
index 0000000000000..59573e5fecf00
--- /dev/null
+++ b/app/views/projects/issues/calendar.ics.haml
@@ -0,0 +1 @@
+= render 'issues/issues_calendar', issues: @issues
diff --git a/app/views/shared/icons/_icon_calendar.svg b/app/views/shared/icons/_icon_calendar.svg
new file mode 100644
index 0000000000000..4d0a703f9a00a
--- /dev/null
+++ b/app/views/shared/icons/_icon_calendar.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M15 5v7a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V5a2 2 0 0 1 2-2h1V2a1 1 0 1 1 2 0v1h4V2a1 1 0 1 1 2 0v1h1a2 2 0 0 1 2 2zM3 6v6a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6H3zm2 2h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z" fill="#000" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
new file mode 100644
index 0000000000000..d4834090413dd
--- /dev/null
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -0,0 +1,4 @@
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do
+  = icon('rss')
+= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do
+  = custom_icon('icon_calendar')
diff --git a/changelogs/unreleased/44184-issues_ical_feed.yml b/changelogs/unreleased/44184-issues_ical_feed.yml
new file mode 100644
index 0000000000000..8151d82625a92
--- /dev/null
+++ b/changelogs/unreleased/44184-issues_ical_feed.yml
@@ -0,0 +1,5 @@
+---
+title: Export assigned issues in iCalendar feed
+merge_request: 17783
+author: Imre Farkas
+type: added
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index d2437285cdf14..f1e8c2b9d8251 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -1,4 +1,5 @@
 resource :dashboard, controller: 'dashboard', only: [] do
+  get :issues, action: :issues_calendar, constraints: lambda { |req| req.format == :ics }
   get :issues
   get :merge_requests
   get :activity
diff --git a/config/routes/group.rb b/config/routes/group.rb
index fff0914c3cdee..b09eb3c1b5b0c 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -5,9 +5,10 @@
 constraints(::Constraints::GroupUrlConstrainer.new) do
   scope(path: 'groups/*id',
         controller: :groups,
-        constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
+        constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom|ics)/ }) do
     scope(path: '-') do
       get :edit, as: :edit_group
+      get :issues, as: :issues_group_calendar, action: :issues_calendar, constraints: lambda { |req| req.format == :ics }
       get :issues, as: :issues_group
       get :merge_requests, as: :merge_requests_group
       get :projects, as: :projects_group
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index a9ba5ac2c0b69..c1cac3905f15b 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -7,7 +7,7 @@
     get :applications, to: 'oauth/applications#index'
 
     put :reset_incoming_email_token
-    put :reset_rss_token
+    put :reset_feed_token
     put :update_username
   end
 
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5a1be1a8b73d2..6dfbd7ecd1ffc 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -353,6 +353,7 @@
         end
       end
 
+      get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics }
       resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
         member do
           post :toggle_subscription
diff --git a/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb
new file mode 100644
index 0000000000000..007cbebaf1bc3
--- /dev/null
+++ b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb
@@ -0,0 +1,15 @@
+class RenameUsersRssTokenToFeedToken < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    rename_column_concurrently :users, :rss_token, :feed_token
+  end
+
+  def down
+    cleanup_concurrent_column_rename :users, :feed_token, :rss_token
+  end
+end
diff --git a/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb
new file mode 100644
index 0000000000000..bff83379087cd
--- /dev/null
+++ b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb
@@ -0,0 +1,13 @@
+class CleanupUsersRssTokenRename < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    cleanup_concurrent_column_rename :users, :rss_token, :feed_token
+  end
+
+  def down
+    rename_column_concurrently :users, :feed_token, :rss_token
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42fea8e43801f..f8663574580e3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2082,9 +2082,9 @@
     t.date "last_activity_on"
     t.boolean "notified_of_own_activity"
     t.string "preferred_language"
-    t.string "rss_token"
     t.integer "theme_id", limit: 2
     t.integer "accepted_term_id"
+    t.string "feed_token"
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -2092,12 +2092,12 @@
   add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
   add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
   add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+  add_index "users", ["feed_token"], name: "index_users_on_feed_token", using: :btree
   add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
   add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
   add_index "users", ["name"], name: "index_users_on_name", using: :btree
   add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
   add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
-  add_index "users", ["rss_token"], name: "index_users_on_rss_token", using: :btree
   add_index "users", ["state"], name: "index_users_on_state", using: :btree
   add_index "users", ["username"], name: "index_users_on_username", using: :btree
   add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index 1bf8b776c2e20..93306437c6ce6 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -39,5 +39,13 @@ The day before an open issue is due, an email will be sent to all participants
 of the issue. Both the due date and the day before are calculated using the
 server's timezone.
 
+Issues with due dates can also be exported as an iCalendar feed. The URL of the
+feed can be added to calendar applications. The feed is accessible by clicking
+on the _Subscribe to calendar_ button on the following pages:
+- on the **Assigned Issues** page that is linked on the right-hand side of the
+  GitLab header
+- on the **Project Issues** page
+- on the **Group Issues** page
+
 [ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
 [permissions]: ../../permissions.md#project
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index a0b5cd868c3b7..66de52506ce87 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -16,7 +16,7 @@ def user
       end
 
       def find_sessionless_user
-        find_user_from_access_token || find_user_from_rss_token
+        find_user_from_access_token || find_user_from_feed_token
       rescue Gitlab::Auth::AuthenticationError
         nil
       end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 4dc23f977da4c..c7993665421a4 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -25,13 +25,15 @@ def find_user_from_warden
         current_request.env['warden']&.authenticate if verified_request?
       end
 
-      def find_user_from_rss_token
-        return unless current_request.path.ends_with?('.atom') || current_request.format.atom?
+      def find_user_from_feed_token
+        return unless rss_request? || ics_request?
 
-        token = current_request.params[:rss_token].presence
+        # NOTE: feed_token was renamed from rss_token but both needs to be supported because
+        #       users might have already added the feed to their RSS reader before the rename
+        token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
         return unless token
 
-        User.find_by_rss_token(token) || raise(UnauthorizedError)
+        User.find_by_feed_token(token) || raise(UnauthorizedError)
       end
 
       def find_user_from_access_token
@@ -104,6 +106,14 @@ def ensure_action_dispatch_request(request)
       def current_request
         @current_request ||= ensure_action_dispatch_request(request)
       end
+
+      def rss_request?
+        current_request.path.ends_with?('.atom') || current_request.format.atom?
+      end
+
+      def ics_request?
+        current_request.path.ends_with?('.ics') || current_request.format.ics?
+      end
     end
   end
 end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 0e27a28ea6e36..72eb8adcce246 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -31,27 +31,27 @@ map $http_upgrade $connection_upgrade_gitlab {
 log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent";
 
 ## Remove private_token from the request URI
-# In:  /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# In:  /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
 map $request_uri $gitlab_temp_request_uri_1 {
   default $request_uri;
   ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
 ## Remove authenticity_token from the request URI
-# In:  /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# In:  /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
 map $gitlab_temp_request_uri_1 $gitlab_temp_request_uri_2 {
   default $gitlab_temp_request_uri_1;
   ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
-## Remove rss_token from the request URI
-# In:  /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+## Remove feed_token from the request URI
+# In:  /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&...
 map $gitlab_temp_request_uri_2 $gitlab_filtered_request_uri {
   default $gitlab_temp_request_uri_2;
-  ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+  ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
 ## A version of the referer without the query string
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 8218d68f9ba80..2e3799d5e1bb0 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -36,27 +36,27 @@ map $http_upgrade $connection_upgrade_gitlab_ssl {
 log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent";
 
 ## Remove private_token from the request URI
-# In:  /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# In:  /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
 map $request_uri $gitlab_ssl_temp_request_uri_1 {
   default $request_uri;
   ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
 ## Remove authenticity_token from the request URI
-# In:  /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# In:  /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
 map $gitlab_ssl_temp_request_uri_1 $gitlab_ssl_temp_request_uri_2 {
   default $gitlab_ssl_temp_request_uri_1;
   ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
-## Remove rss_token from the request URI
-# In:  /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
-# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+## Remove feed_token from the request URI
+# In:  /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&...
 map $gitlab_ssl_temp_request_uri_2 $gitlab_ssl_filtered_request_uri {
   default $gitlab_ssl_temp_request_uri_2;
-  ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+  ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
 }
 
 ## A version of the referer without the query string
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 693597afdf8e1..81829668de8fd 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -6,9 +6,9 @@ namespace :tokens do
     reset_all_users_token(:reset_incoming_email_token!)
   end
 
-  desc "Reset all GitLab RSS tokens"
-  task reset_all_rss: :environment do
-    reset_all_users_token(:reset_rss_token!)
+  desc "Reset all GitLab feed tokens"
+  task reset_all_feed: :environment do
+    reset_all_users_token(:reset_feed_token!)
   end
 
   def reset_all_users_token(reset_token_method)
@@ -31,8 +31,8 @@ class TmpUser < ActiveRecord::Base
     save!(validate: false)
   end
 
-  def reset_rss_token!
-    write_new_token(:rss_token)
+  def reset_feed_token!
+    write_new_token(:feed_token)
     save!(validate: false)
   end
 end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index f0caac40afd1c..b048da1991ce0 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -146,35 +146,43 @@ def index
     end
   end
 
-  describe '#authenticate_user_from_rss_token' do
-    describe "authenticating a user from an RSS token" do
+  describe '#authenticate_sessionless_user!' do
+    describe 'authenticating a user from a feed token' do
       controller(described_class) do
         def index
           render text: 'authenticated'
         end
       end
 
-      context "when the 'rss_token' param is populated with the RSS token" do
+      context "when the 'feed_token' param is populated with the feed token" do
         context 'when the request format is atom' do
           it "logs the user in" do
-            get :index, rss_token: user.rss_token, format: :atom
+            get :index, feed_token: user.feed_token, format: :atom
             expect(response).to have_gitlab_http_status 200
             expect(response.body).to eq 'authenticated'
           end
         end
 
-        context 'when the request format is not atom' do
+        context 'when the request format is ics' do
+          it "logs the user in" do
+            get :index, feed_token: user.feed_token, format: :ics
+            expect(response).to have_gitlab_http_status 200
+            expect(response.body).to eq 'authenticated'
+          end
+        end
+
+        context 'when the request format is neither atom nor ics' do
           it "doesn't log the user in" do
-            get :index, rss_token: user.rss_token
+            get :index, feed_token: user.feed_token
             expect(response.status).not_to have_gitlab_http_status 200
             expect(response.body).not_to eq 'authenticated'
           end
         end
       end
 
-      context "when the 'rss_token' param is populated with an invalid RSS token" do
+      context "when the 'feed_token' param is populated with an invalid feed token" do
         it "doesn't log the user" do
-          get :index, rss_token: "token"
+          get :index, feed_token: 'token', format: :atom
           expect(response.status).not_to eq 200
           expect(response.body).not_to eq 'authenticated'
         end
@@ -454,7 +462,7 @@ def index
         end
 
         it 'renders a 403 when the sessionless user did not accept the terms' do
-          get :index, rss_token: user.rss_token, format: :atom
+          get :index, feed_token: user.feed_token, format: :atom
 
           expect(response).to have_gitlab_http_status(403)
         end
@@ -462,7 +470,7 @@ def index
         it 'renders a 200 when the sessionless user accepted the terms' do
           accept_terms(user)
 
-          get :index, rss_token: user.rss_token, format: :atom
+          get :index, feed_token: user.feed_token, format: :atom
 
           expect(response).to have_gitlab_http_status(200)
         end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 769fd656e7a4a..59db8cdc34bfb 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -12,10 +12,6 @@
       user.notification_email = user.email
     end
 
-    before(:create) do |user|
-      user.ensure_rss_token
-    end
-
     trait :admin do
       admin true
     end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index fb6c71ce9976e..da7749b42d259 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -31,20 +31,20 @@
         expect(body).to have_selector('title', text: "#{user.name} issues")
       end
 
-      it "renders atom feed via RSS token" do
-        visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: user.id)
+      it "renders atom feed via feed token" do
+        visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: user.id)
 
         expect(response_headers['Content-Type']).to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{user.name} issues")
       end
 
       it "renders atom feed with url parameters" do
-        visit issues_dashboard_path(:atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+        visit issues_dashboard_path(:atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
 
         link = find('link[type="application/atom+xml"]')
         params = CGI.parse(URI.parse(link[:href]).query)
 
-        expect(params).to include('rss_token' => [user.rss_token])
+        expect(params).to include('feed_token' => [user.feed_token])
         expect(params).to include('state' => ['opened'])
         expect(params).to include('assignee_id' => [user.id.to_s])
       end
@@ -53,7 +53,7 @@
         let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
 
         it "renders issue fields" do
-          visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
+          visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
 
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
 
@@ -76,7 +76,7 @@
         end
 
         it "renders issue label and milestone info" do
-          visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id)
+          visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
 
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
 
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index c6683bb3bc9b8..462eab07a7547 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -13,9 +13,9 @@
       end
     end
 
-    context "projects atom feed via RSS token" do
+    context "projects atom feed via feed token" do
       it "renders projects atom feed" do
-        visit dashboard_projects_path(:atom, rss_token: user.rss_token)
+        visit dashboard_projects_path(:atom, feed_token: user.feed_token)
         expect(body).to have_selector('feed title')
       end
     end
@@ -29,7 +29,7 @@
         project.add_master(user)
         issue_event(issue, user)
         note_event(note, user)
-        visit dashboard_projects_path(:atom, rss_token: user.rss_token)
+        visit dashboard_projects_path(:atom, feed_token: user.feed_token)
       end
 
       it "has issue opened event" do
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 525ce23aa56a0..ee3570a5b2be4 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -45,10 +45,10 @@
       end
     end
 
-    context 'when authenticated via RSS token' do
+    context 'when authenticated via feed token' do
       it 'renders atom feed' do
         visit project_issues_path(project, :atom,
-                                            rss_token: user.rss_token)
+                                            feed_token: user.feed_token)
 
         expect(response_headers['Content-Type'])
           .to have_content('application/atom+xml')
@@ -61,24 +61,23 @@
     end
 
     it "renders atom feed with url parameters for project issues" do
-      visit project_issues_path(project,
-                                          :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+      visit project_issues_path(project, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
 
       link = find('link[type="application/atom+xml"]')
       params = CGI.parse(URI.parse(link[:href]).query)
 
-      expect(params).to include('rss_token' => [user.rss_token])
+      expect(params).to include('feed_token' => [user.feed_token])
       expect(params).to include('state' => ['opened'])
       expect(params).to include('assignee_id' => [user.id.to_s])
     end
 
     it "renders atom feed with url parameters for group issues" do
-      visit issues_group_path(group, :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
+      visit issues_group_path(group, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
 
       link = find('link[type="application/atom+xml"]')
       params = CGI.parse(URI.parse(link[:href]).query)
 
-      expect(params).to include('rss_token' => [user.rss_token])
+      expect(params).to include('feed_token' => [user.feed_token])
       expect(params).to include('state' => ['opened'])
       expect(params).to include('assignee_id' => [user.id.to_s])
     end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 2d074c115ddca..eeaaa40fe2175 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -13,9 +13,9 @@
       end
     end
 
-    context 'user atom feed via RSS token' do
+    context 'user atom feed via feed token' do
       it "renders user atom feed" do
-        visit user_path(user, :atom, rss_token: user.rss_token)
+        visit user_path(user, :atom, feed_token: user.feed_token)
         expect(body).to have_selector('feed title')
       end
     end
@@ -51,7 +51,7 @@
         issue_event(issue, user)
         note_event(note, user)
         merge_request_event(merge_request, user)
-        visit user_path(user, :atom, rss_token: user.rss_token)
+        visit user_path(user, :atom, feed_token: user.feed_token)
       end
 
       it 'has issue opened event' do
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index a74a8aac2b2db..941208fa2448e 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -12,8 +12,8 @@
       visit activity_dashboard_path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   context 'event filters', :js do
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index bab34ac9346be..8d0b0be1bd42f 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -47,15 +47,15 @@
     it 'updates atom feed link' do
       visit_issues(milestone_title: '', assignee_id: user.id)
 
-      link = find('.nav-controls a[title="Subscribe"]')
+      link = find('.nav-controls a[title="Subscribe to RSS feed"]')
       params = CGI.parse(URI.parse(link[:href]).query)
       auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
       auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
 
-      expect(params).to include('rss_token' => [user.rss_token])
+      expect(params).to include('feed_token' => [user.feed_token])
       expect(params).to include('milestone_title' => [''])
       expect(params).to include('assignee_id' => [user.id.to_s])
-      expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
+      expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
       expect(auto_discovery_params).to include('milestone_title' => [''])
       expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
     end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index e41a2e4ce091c..3cc7b38550d2f 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -56,8 +56,8 @@
       expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true)
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   describe 'new issue dropdown' do
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 257a38225037a..ef2f0b5b31a32 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -10,7 +10,7 @@
     sign_in(user)
   end
 
-  it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" do
+  it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
     before do
       visit dashboard_projects_path
     end
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index 7bc809b310409..0d7d377107161 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -15,8 +15,8 @@
         visit path
       end
 
-      it_behaves_like "it has an RSS button with current_user's RSS token"
-      it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+      it_behaves_like "it has an RSS button with current_user's feed token"
+      it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
     end
 
     context 'when project is in the group', :js do
@@ -39,7 +39,7 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button without an RSS token"
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    it_behaves_like "it has an RSS button without a feed token"
+    it_behaves_like "an autodiscoverable RSS feed without a feed token"
   end
 end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 90bf7ba49f6ed..111a24c0d9471 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -16,17 +16,21 @@
       let(:access_level) { ProjectFeature::ENABLED }
 
       context 'when signed in' do
-        let(:user) { user_in_group }
-
-        it_behaves_like "it has an RSS button with current_user's RSS token"
-        it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+        let(:user) do
+          user_in_group.ensure_feed_token
+          user_in_group.save!
+          user_in_group
+        end
+
+        it_behaves_like "it has an RSS button with current_user's feed token"
+        it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
       end
 
       context 'when signed out' do
         let(:user) { nil }
 
-        it_behaves_like "it has an RSS button without an RSS token"
-        it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+        it_behaves_like "it has an RSS button without a feed token"
+        it_behaves_like "an autodiscoverable RSS feed without a feed token"
       end
     end
 
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 3a0424d60f8a5..b7a7aa0e17400 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -14,7 +14,7 @@
       visit path
     end
 
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
 
     context 'when group does not exist' do
       let(:path) { group_path('not-exist') }
@@ -29,7 +29,7 @@
         visit path
       end
 
-      it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+      it_behaves_like "an autodiscoverable RSS feed without a feed token"
     end
 
     context 'when group has a public project', :js do
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
new file mode 100644
index 0000000000000..5d6cd44ad1cca
--- /dev/null
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe 'Dashboard Issues Calendar Feed'  do
+  describe 'GET /issues' do
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+    let!(:project) { create(:project) }
+
+    before do
+      project.add_master(user)
+    end
+
+    context 'when authenticated' do
+      it 'renders calendar feed' do
+        sign_in user
+        visit issues_dashboard_path(:ics)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via personal access token' do
+      it 'renders calendar feed' do
+        personal_access_token = create(:personal_access_token, user: user)
+
+        visit issues_dashboard_path(:ics, private_token: personal_access_token.token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via feed token' do
+      it 'renders calendar feed' do
+        visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'issue with due date' do
+      let!(:issue) do
+        create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+                       description: 'test desc', due_date: Date.tomorrow)
+      end
+
+      it 'renders issue fields' do
+        visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+
+        expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+        # line length for ics is 75 chars
+        expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+        expect(body).to have_text(expected_description)
+        expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+        expect(body).to have_text("URL:#{issue_url(issue)}")
+        expect(body).to have_text('TRANSP:TRANSPARENT')
+      end
+    end
+  end
+end
diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb
new file mode 100644
index 0000000000000..0a049be2ffe76
--- /dev/null
+++ b/spec/features/ics/group_issues_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'Group Issues Calendar Feed'  do
+  describe 'GET /issues' do
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+    let!(:group)    { create(:group) }
+    let!(:project)  { create(:project, group: group) }
+
+    before do
+      project.add_developer(user)
+      group.add_developer(user)
+    end
+
+    context 'when authenticated' do
+      it 'renders calendar feed' do
+        sign_in user
+        visit issues_group_path(group, :ics)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via personal access token' do
+      it 'renders calendar feed' do
+        personal_access_token = create(:personal_access_token, user: user)
+
+        visit issues_group_path(group, :ics, private_token: personal_access_token.token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via feed token' do
+      it 'renders calendar feed' do
+        visit issues_group_path(group, :ics, feed_token: user.feed_token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'issue with due date' do
+      let!(:issue) do
+        create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+                       description: 'test desc', due_date: Date.tomorrow)
+      end
+
+      it 'renders issue fields' do
+        visit issues_group_path(group, :ics, feed_token: user.feed_token)
+
+        expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+        # line length for ics is 75 chars
+        expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+        expect(body).to have_text(expected_description)
+        expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+        expect(body).to have_text("URL:#{issue_url(issue)}")
+        expect(body).to have_text('TRANSP:TRANSPARENT')
+      end
+    end
+  end
+end
diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb
new file mode 100644
index 0000000000000..b99e9607f1d23
--- /dev/null
+++ b/spec/features/ics/project_issues_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe 'Project Issues Calendar Feed'  do
+  describe 'GET /issues' do
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
+    let!(:project)  { create(:project) }
+    let!(:issue)    { create(:issue, author: user, assignees: [assignee], project: project) }
+
+    before do
+      project.add_developer(user)
+    end
+
+    context 'when authenticated' do
+      it 'renders calendar feed' do
+        sign_in user
+        visit project_issues_path(project, :ics)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via personal access token' do
+      it 'renders calendar feed' do
+        personal_access_token = create(:personal_access_token, user: user)
+
+        visit project_issues_path(project, :ics, private_token: personal_access_token.token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'when authenticated via feed token' do
+      it 'renders calendar feed' do
+        visit project_issues_path(project, :ics, feed_token: user.feed_token)
+
+        expect(response_headers['Content-Type']).to have_content('text/calendar')
+        expect(response_headers['Content-Disposition']).to have_content('inline')
+        expect(body).to have_text('BEGIN:VCALENDAR')
+      end
+    end
+
+    context 'issue with due date' do
+      let!(:issue) do
+        create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
+                       description: 'test desc', due_date: Date.tomorrow)
+      end
+
+      it 'renders issue fields' do
+        visit project_issues_path(project, :ics, feed_token: user.feed_token)
+
+        expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
+        # line length for ics is 75 chars
+        expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n")
+        expect(body).to have_text(expected_description)
+        expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}")
+        expect(body).to have_text("URL:#{issue_url(issue)}")
+        expect(body).to have_text('TRANSP:TRANSPARENT')
+      end
+    end
+  end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 483122ae46376..bc42618306f1d 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -468,13 +468,13 @@ def expect_no_issues_list
       it "for #{type}" do
         visit path
 
-        link = find_link('Subscribe')
+        link = find_link('Subscribe to RSS feed')
         params = CGI.parse(URI.parse(link[:href]).query)
         auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
         auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
 
         expected = {
-          'rss_token' => [user.rss_token],
+          'feed_token' => [user.feed_token],
           'milestone_title' => [milestone.title],
           'assignee_id' => [user.id.to_s]
         }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index e7f2e142b2d9d..c6dcd97631d8b 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -340,6 +340,20 @@
             expect(page).to have_content('baz')
           end
         end
+
+        it 'filters by due next month and previous two weeks' do
+          foo.update(due_date: Date.today - 4.weeks)
+          bar.update(due_date: (Date.today + 2.months).beginning_of_month)
+          baz.update(due_date: Date.yesterday)
+
+          visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name)
+
+          page.within '.issues-holder' do
+            expect(page).not_to have_content('foo')
+            expect(page).not_to have_content('bar')
+            expect(page).to have_content('baz')
+          end
+        end
       end
 
       describe 'sorting by milestone' do
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 15dcb30cbddc8..2e0753c3bfbc9 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -56,21 +56,21 @@
     end
   end
 
-  describe 'when I reset RSS token' do
+  describe 'when I reset feed token' do
     before do
       visit profile_personal_access_tokens_path
     end
 
-    it 'resets RSS token' do
-      within('.rss-token-reset') do
-        previous_token = find("#rss_token").value
+    it 'resets feed token' do
+      within('.feed-token-reset') do
+        previous_token = find("#feed_token").value
 
         accept_confirm { click_link('reset it') }
 
-        expect(find('#rss_token').value).not_to eq(previous_token)
+        expect(find('#feed_token').value).not_to eq(previous_token)
       end
 
-      expect(page).to have_content 'RSS token was successfully reset'
+      expect(page).to have_content 'Feed token was successfully reset'
     end
   end
 
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index cd1cfe0799864..4ac34adde0ede 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -15,7 +15,7 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -23,6 +23,6 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button without an RSS token"
+    it_behaves_like "it has an RSS button without a feed token"
   end
 end
diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb
index 0d9c7355ddda3..0bc207da9703c 100644
--- a/spec/features/projects/commits/rss_spec.rb
+++ b/spec/features/projects/commits/rss_spec.rb
@@ -12,8 +12,8 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -21,7 +21,7 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button without an RSS token"
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    it_behaves_like "it has an RSS button without a feed token"
+    it_behaves_like "an autodiscoverable RSS feed without a feed token"
   end
 end
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
index ff91aabc311e8..8b1f7d432ee5c 100644
--- a/spec/features/projects/issues/rss_spec.rb
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -17,8 +17,8 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -26,7 +26,7 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button without an RSS token"
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    it_behaves_like "it has an RSS button without a feed token"
+    it_behaves_like "an autodiscoverable RSS feed without a feed token"
   end
 end
diff --git a/spec/features/projects/show/rss_spec.rb b/spec/features/projects/show/rss_spec.rb
index d02eaf3453357..52164d30c4034 100644
--- a/spec/features/projects/show/rss_spec.rb
+++ b/spec/features/projects/show/rss_spec.rb
@@ -12,7 +12,7 @@
       visit path
     end
 
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -20,6 +20,6 @@
       visit path
     end
 
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    it_behaves_like "an autodiscoverable RSS feed without a feed token"
   end
 end
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
index 6407370ac0dc1..f52b3cc1d86c4 100644
--- a/spec/features/projects/tree/rss_spec.rb
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -12,7 +12,7 @@
       visit path
     end
 
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -20,6 +20,6 @@
       visit path
     end
 
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    it_behaves_like "an autodiscoverable RSS feed without a feed token"
   end
 end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 7c5abe54d560a..c3734b5c80844 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -10,7 +10,7 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
+    it_behaves_like "it has an RSS button with current_user's feed token"
   end
 
   context 'when signed out' do
@@ -18,6 +18,6 @@
       visit path
     end
 
-    it_behaves_like "it has an RSS button without an RSS token"
+    it_behaves_like "it has an RSS button without a feed token"
   end
 end
diff --git a/spec/helpers/calendar_helper_spec.rb b/spec/helpers/calendar_helper_spec.rb
new file mode 100644
index 0000000000000..828a9d9fea098
--- /dev/null
+++ b/spec/helpers/calendar_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe CalendarHelper do
+  describe '#calendar_url_options' do
+    context 'when signed in' do
+      it "includes the current_user's feed_token" do
+        current_user = create(:user)
+        allow(helper).to receive(:current_user).and_return(current_user)
+        expect(helper.calendar_url_options).to include feed_token: current_user.feed_token
+      end
+    end
+
+    context 'when signed out' do
+      it "does not have a feed_token" do
+        allow(helper).to receive(:current_user).and_return(nil)
+        expect(helper.calendar_url_options[:feed_token]).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
index 269e1057e8d1d..a7f9bdf07e4f6 100644
--- a/spec/helpers/rss_helper_spec.rb
+++ b/spec/helpers/rss_helper_spec.rb
@@ -3,17 +3,17 @@
 describe RssHelper do
   describe '#rss_url_options' do
     context 'when signed in' do
-      it "includes the current_user's rss_token" do
+      it "includes the current_user's feed_token" do
         current_user = create(:user)
         allow(helper).to receive(:current_user).and_return(current_user)
-        expect(helper.rss_url_options).to include rss_token: current_user.rss_token
+        expect(helper.rss_url_options).to include feed_token: current_user.feed_token
       end
     end
 
     context 'when signed out' do
-      it "does not have an rss_token" do
+      it "does not have a feed_token" do
         allow(helper).to receive(:current_user).and_return(nil)
-        expect(helper.rss_url_options[:rss_token]).to be_nil
+        expect(helper.rss_url_options[:feed_token]).to be_nil
       end
     end
   end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index ffcd90b9fcb1e..242ab4a91dd83 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -39,19 +39,19 @@
 
   describe '#find_sessionless_user' do
     let!(:access_token_user) { build(:user) }
-    let!(:rss_token_user) { build(:user) }
+    let!(:feed_token_user) { build(:user) }
 
     it 'returns access_token user first' do
       allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user)
-      allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+      allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
 
       expect(subject.find_sessionless_user).to eq access_token_user
     end
 
-    it 'returns rss_token user if no access_token user found' do
-      allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+    it 'returns feed_token user if no access_token user found' do
+      allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
 
-      expect(subject.find_sessionless_user).to eq rss_token_user
+      expect(subject.find_sessionless_user).to eq feed_token_user
     end
 
     it 'returns nil if no user found' do
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
index 2733eef66117a..136646bd4ee54 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -46,34 +46,54 @@ def set_param(key, value)
     end
   end
 
-  describe '#find_user_from_rss_token' do
+  describe '#find_user_from_feed_token' do
     context 'when the request format is atom' do
       before do
         env['HTTP_ACCEPT'] = 'application/atom+xml'
       end
 
-      it 'returns user if valid rss_token' do
-        set_param(:rss_token, user.rss_token)
+      context 'when feed_token param is provided' do
+        it 'returns user if valid feed_token' do
+          set_param(:feed_token, user.feed_token)
 
-        expect(find_user_from_rss_token).to eq user
-      end
+          expect(find_user_from_feed_token).to eq user
+        end
+
+        it 'returns nil if feed_token is blank' do
+          expect(find_user_from_feed_token).to be_nil
+        end
+
+        it 'returns exception if invalid feed_token' do
+          set_param(:feed_token, 'invalid_token')
 
-      it 'returns nil if rss_token is blank' do
-        expect(find_user_from_rss_token).to be_nil
+          expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+        end
       end
 
-      it 'returns exception if invalid rss_token' do
-        set_param(:rss_token, 'invalid_token')
+      context 'when rss_token param is provided' do
+        it 'returns user if valid rssd_token' do
+          set_param(:rss_token, user.feed_token)
 
-        expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+          expect(find_user_from_feed_token).to eq user
+        end
+
+        it 'returns nil if rss_token is blank' do
+          expect(find_user_from_feed_token).to be_nil
+        end
+
+        it 'returns exception if invalid rss_token' do
+          set_param(:rss_token, 'invalid_token')
+
+          expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+        end
       end
     end
 
     context 'when the request format is not atom' do
       it 'returns nil' do
-        set_param(:rss_token, user.rss_token)
+        set_param(:feed_token, user.feed_token)
 
-        expect(find_user_from_rss_token).to be_nil
+        expect(find_user_from_feed_token).to be_nil
       end
     end
 
@@ -81,7 +101,7 @@ def set_param(key, value)
       it 'the method call does not modify the original value' do
         env['action_dispatch.request.formats'] = nil
 
-        find_user_from_rss_token
+        find_user_from_feed_token
 
         expect(env['action_dispatch.request.formats']).to be_nil
       end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index dfb83578fce4f..9b80442913811 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -12,7 +12,7 @@
 end
 
 describe User, 'TokenAuthenticatable' do
-  let(:token_field) { :rss_token }
+  let(:token_field) { :feed_token }
   it_behaves_like 'TokenAuthenticatable'
 
   describe 'ensures authentication token' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6a2f4a39f09dc..16b409844fa82 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -644,13 +644,13 @@
     end
   end
 
-  describe 'rss token' do
-    it 'ensures an rss token on read' do
-      user = create(:user, rss_token: nil)
-      rss_token = user.rss_token
+  describe 'feed token' do
+    it 'ensures a feed token on read' do
+      user = create(:user, feed_token: nil)
+      feed_token = user.feed_token
 
-      expect(rss_token).not_to be_blank
-      expect(user.reload.rss_token).to eq rss_token
+      expect(feed_token).not_to be_blank
+      expect(user.reload.feed_token).to eq feed_token
     end
   end
 
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index b18e922b0630d..c0a3ea397df6a 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -349,7 +349,7 @@ def api_get_args_with_token_headers(partial_url, token_headers)
   end
 
   def rss_url(user)
-    "/dashboard/projects.atom?rss_token=#{user.rss_token}"
+    "/dashboard/projects.atom?feed_token=#{user.feed_token}"
   end
 
   def private_token_headers(user)
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 9345671a1a7fe..dd8f6239587b7 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -162,8 +162,8 @@
     expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
   end
 
-  it "to #reset_rss_token" do
-    expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
+  it "to #reset_feed_token" do
+    expect(put("/profile/reset_feed_token")).to route_to('profiles#reset_feed_token')
   end
 
   it "to #show" do
@@ -249,7 +249,11 @@
   end
 
   it "to #issues" do
-    expect(get("/dashboard/issues")).to route_to('dashboard#issues')
+    expect(get("/dashboard/issues.html")).to route_to('dashboard#issues', format: 'html')
+  end
+
+  it "to #calendar_issues" do
+    expect(get("/dashboard/issues.ics")).to route_to('dashboard#issues_calendar', format: 'ics')
   end
 
   it "to #merge_requests" do
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index 50fbbc7f55b56..0de92aedba512 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -1,23 +1,23 @@
-shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do
-  it "has an RSS autodiscovery link tag with current_user's RSS token" do
-    expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{user.rss_token}']", visible: false)
+shared_examples "an autodiscoverable RSS feed with current_user's feed token" do
+  it "has an RSS autodiscovery link tag with current_user's feed token" do
+    expect(page).to have_css("link[type*='atom+xml'][href*='feed_token=#{user.feed_token}']", visible: false)
   end
 end
 
-shared_examples "it has an RSS button with current_user's RSS token" do
-  it "shows the RSS button with current_user's RSS token" do
-    expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{user.rss_token}']")
+shared_examples "it has an RSS button with current_user's feed token" do
+  it "shows the RSS button with current_user's feed token" do
+    expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
   end
 end
 
-shared_examples "an autodiscoverable RSS feed without an RSS token" do
-  it "has an RSS autodiscovery link tag without an RSS token" do
-    expect(page).to have_css("link[type*='atom+xml']:not([href*='rss_token'])", visible: false)
+shared_examples "an autodiscoverable RSS feed without a feed token" do
+  it "has an RSS autodiscovery link tag without a feed token" do
+    expect(page).to have_css("link[type*='atom+xml']:not([href*='feed_token'])", visible: false)
   end
 end
 
-shared_examples "it has an RSS button without an RSS token" do
-  it "shows the RSS button without an RSS token" do
-    expect(page).to have_css("a:has(.fa-rss):not([href*='rss_token'])")
+shared_examples "it has an RSS button without a feed token" do
+  it "shows the RSS button without a feed token" do
+    expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
   end
 end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 51f7a536cbb98..555a58e9aa1f5 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -13,9 +13,9 @@
     end
   end
 
-  describe 'reset_all_rss task' do
+  describe 'reset_all_feed task' do
     it 'invokes create_hooks task' do
-      expect { run_rake_task('tokens:reset_all_rss') }.to change { user.reload.rss_token }
+      expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token }
     end
   end
 end
-- 
GitLab