summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorImre Farkas <ifarkas@gitlab.com>2018-05-31 14:01:04 +0000
committerDouwe Maan <douwe@gitlab.com>2018-05-31 14:01:04 +0000
commit20dfe25c151cc883ce0d38b67125b5ca41e6d422 (patch)
tree9a29f05a241713f3488e6bc2e5df03c07300ef45 /app
parent2fdd8982f8204340e6413a57f46e6c41d8ecb429 (diff)
downloadgitlab-ce-20dfe25c151cc883ce0d38b67125b5ca41e6d422.tar.gz
Export assigned issues in iCalendar feed
Diffstat (limited to 'app')
-rw-r--r--app/controllers/concerns/issues_action.rb15
-rw-r--r--app/controllers/profiles_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb15
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/helpers/calendar_helper.rb8
-rw-r--r--app/helpers/rss_helper.rb2
-rw-r--r--app/models/issue.rb16
-rw-r--r--app/models/user.rb8
-rw-r--r--app/views/dashboard/issues.html.haml3
-rw-r--r--app/views/dashboard/issues_calendar.ics.haml1
-rw-r--r--app/views/groups/issues.html.haml5
-rw-r--r--app/views/groups/issues_calendar.ics.haml1
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby15
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml14
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml4
-rw-r--r--app/views/projects/issues/calendar.ics.haml1
-rw-r--r--app/views/shared/icons/_icon_calendar.svg1
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml4
18 files changed, 93 insertions, 32 deletions
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 3b11a373368..b6eb7d292fc 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -17,10 +17,23 @@ module IssuesAction
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 9f5ad23a20f..074db361949 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController
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 d69015c8665..35c36c725e2 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 @@ class Projects::IssuesController < Projects::ApplicationController
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 1787b4899cd..3626670d141 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder
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 @@ class IssuesFinder < IssuableFinder
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 00000000000..c54b91b0ce5
--- /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 9ac4df88dc3..7d4fa83a67a 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 0332bfa9371..559770fa442 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 @@ class Issue < ActiveRecord::Base
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 0a838d34054..e219ab800ad 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 @@ class User < ActiveRecord::Base
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 4bf04dadf01..86a21e24ac9 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 00000000000..59573e5fecf
--- /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 662db18cf86..8037cf4b69d 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 00000000000..59573e5fecf
--- /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 00000000000..3563635d33d
--- /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 d253e8e456e..d111113c646 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 297b928f020..0dd2d2e6c5d 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 00000000000..59573e5fecf
--- /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 00000000000..4d0a703f9a0
--- /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 00000000000..d4834090413
--- /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')