summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG1
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb22
-rw-r--r--app/helpers/issues_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb18
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/views/projects/issues/_issue.html.haml4
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml7
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml25
-rw-r--r--db/migrate/20160310124959_add_due_date_to_issues.rb5
-rw-r--r--db/schema.rb15
-rw-r--r--spec/features/issues_spec.rb63
15 files changed, 185 insertions, 1 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 9cfa04b1e63..b876e027132 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -267,6 +267,7 @@ v 8.5.7
v 8.5.6
- Obtain a lease before querying LDAP
+ - Add ability set due date to issues, sort and filter issues by due date
v 8.5.5
- Ensure removing a project removes associated Todo entries
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e86428147ef..b96ab91c17d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -192,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :state_event, :task_num, label_ids: []
+ :milestone_id, :due_date, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 5eb1d3f5aac..5e08193b5cf 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -39,6 +39,7 @@ class IssuableFinder
items = by_assignee(items)
items = by_author(items)
items = by_label(items)
+ items = by_due_date(items)
sort(items)
end
@@ -112,6 +113,14 @@ class IssuableFinder
end
end
+ def due_date?
+ params[:due_date].present?
+ end
+
+ def filter_by_no_due_date?
+ due_date? && params[:due_date] == Issue::NO_DUE_DATE[1]
+ end
+
def labels?
params[:label_name].present?
end
@@ -283,6 +292,19 @@ class IssuableFinder
items.distinct
end
+ def by_due_date(items)
+ if due_date?
+ if filter_by_no_due_date?
+ items = items.no_due_date
+ else
+ items = items.has_due_date
+ # Must use issues prefix to avoid ambiguous match with Milestone#due_date
+ items = items.where("issues.due_date > ? AND issues.due_date <= ?", Date.today, params[:due_date])
+ end
+ end
+ items
+ end
+
def label_names
params[:label_name].split(',')
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4cb8adcebad..2a193e12ec9 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -172,6 +172,17 @@ module IssuesHelper
end.to_h
end
+ def due_date_options
+ options = [
+ ["Due to tomorrow", 1.day.from_now.to_date],
+ ["Due in this week", 1.week.from_now.to_date]
+ ]
+ options.unshift(Issue::ANY_DUE_DATE)
+ options.unshift(Issue::NO_DUE_DATE)
+ options_for_select(options, params[:due_date])
+ end
+
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 2f2d2721d6d..624cb7bb847 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later'
end
+ def sort_title_due_date_soon
+ 'Due date soon'
+ end
+
+ def sort_title_due_date_later
+ 'Due date due later'
+ end
+
def sort_title_name
'Name'
end
@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc'
end
+ def sort_value_due_date_soon
+ 'due_date_asc'
+ end
+
+ def sort_value_due_date_later
+ 'due_date_desc'
+ end
+
def sort_value_name
'name_asc'
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index afa2ca039ae..691b7e104e4 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -39,6 +39,8 @@ module Issuable
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
+ scope :has_due_date, ->{ where("issues.due_date IS NOT NULL") }
+ scope :no_due_date, ->{ where("issues.due_date IS NULL")}
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd..c88a8f5ceb8 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -18,6 +18,8 @@ module Sortable
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :due_date_asc, -> { reorder(due_date: :asc) }
+ scope :due_date_desc, -> { reorder("due_date IS NULL, due_date DESC") }
end
module ClassMethods
@@ -31,6 +33,8 @@ module Sortable
when 'created_desc' then order_created_desc
when 'id_desc' then order_id_desc
when 'id_asc' then order_id_asc
+ when 'due_date_asc' then due_date_asc
+ when 'due_date_desc' then due_date_desc
else
all
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index a009e235b37..ee5be904330 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -28,6 +28,9 @@ class Issue < ActiveRecord::Base
include Sortable
include Taskable
+ NO_DUE_DATE = ['No Due Date', '0']
+ ANY_DUE_DATE = ['Any Due Date', '']
+
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 7a8009f6da4..c4feb6d3e18 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -48,6 +48,10 @@
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o')
= issue.milestone.title
+ - if issue.due_date
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index e3a6a5a68b6..80971309da7 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -20,6 +20,10 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later
+ = link_to page_filter_path(sort: sort_value_due_date_soon) do
+ = sort_title_due_date_soon if controller_name == "issues"
+ = link_to page_filter_path(sort: sort_value_due_date_later) do
+ = sort_title_due_date_later if controller_name == "issues"
= link_to page_filter_path(sort: sort_value_upvotes) do
= sort_title_upvotes
= link_to page_filter_path(sort: sort_value_downvotes) do
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ade0a56b2e7..f832f430b2b 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -23,6 +23,13 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown"
+
+ - if controller.controller_name == 'issues'
+ .filter-item.inline.due_date-filter
+ = select_tag('due_date', due_date_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Due Date'})
+
.pull-right
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 03a615d191c..fb2c727d57a 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -74,6 +74,31 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
+ - if issuable.has_attribute? :due_date
+ .block.due_date
+ .sidebar-collapsed-icon
+ = icon('calendar')
+ %span
+ - if issuable.due_date
+ = icon('calendar')
+ = issuable.due_date.to_s(:medium)
+ - else
+ .light None
+ .title.hide-collapsed
+ %label
+ Due Date
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.hide-collapsed
+ - if issuable.due_date
+ = icon('calendar')
+ = issuable.due_date.to_s(:medium)
+ - else
+ .light None
+ .selectbox.hide-collapsed
+ = f.text_field :due_date
+ = hidden_field_tag :issuable_context
- if issuable.project.labels.any?
.block.labels
diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb
new file mode 100644
index 00000000000..c232387a6f3
--- /dev/null
+++ b/db/migrate/20160310124959_add_due_date_to_issues.rb
@@ -0,0 +1,5 @@
+class AddDueDateToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :due_date, :date
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d82c8c1e257..699a99c0743 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -366,6 +366,19 @@ ActiveRecord::Schema.define(version: 20160419120017) do
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
+ create_table "emoji_awards", force: :cascade do |t|
+ t.string "name"
+ t.integer "user_id"
+ t.integer "awardable_id"
+ t.string "awardable_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "emoji_awards", ["awardable_id"], name: "index_emoji_awards_on_awardable_id", using: :btree
+ add_index "emoji_awards", ["awardable_type"], name: "index_emoji_awards_on_awardable_type", using: :btree
+ add_index "emoji_awards", ["user_id"], name: "index_emoji_awards_on_user_id", using: :btree
+
create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
@@ -422,6 +435,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
t.integer "moved_to_id"
t.boolean "confidential", default: false
t.datetime "deleted_at"
+ t.date "due_date"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -431,6 +445,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 35c8f93abc1..ac54a0c2719 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -153,6 +153,69 @@ describe 'Issues', feature: true do
expect(first_issue).to include('baz')
end
+ describe 'sorting by due date' do
+ before :each do
+ foo.due_date = 1.day.from_now
+ foo.save
+ bar.due_date = 6.days.from_now
+ bar.save
+ end
+
+ it 'sorts by recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon)
+ expect(first_issue).to include('foo')
+ end
+
+ it 'sorts by least recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+ expect(first_issue).to include('bar')
+ end
+
+ it 'sorts by least recently due date by excluding nil due dates' do
+ bar.update(due_date: nil)
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+ expect(first_issue).to include('foo')
+ end
+ end
+
+ describe 'filtering by due date' do
+ before :each do
+ foo.due_date = 1.day.from_now
+ foo.save
+ bar.due_date = 6.days.from_now
+ bar.save
+ end
+
+ it 'filters by none' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NO_DUE_DATE[1])
+ expect(page).not_to have_content("foo")
+ expect(page).not_to have_content("bar")
+ expect(page).to have_content("baz")
+ end
+
+ it 'filters by any' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::ANY_DUE_DATE[1])
+ expect(page).to have_content("foo")
+ expect(page).to have_content("bar")
+ expect(page).to have_content("baz")
+ end
+
+ it 'filters by due to tomorrow' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Date.tomorrow.to_s)
+ expect(page).to have_content("foo")
+ expect(page).not_to have_content("bar")
+ expect(page).not_to have_content("baz")
+ end
+
+ it 'filters by due in this week' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: 7.days.from_now.to_date.to_s)
+ expect(page).to have_content("foo")
+ expect(page).to have_content("bar")
+ expect(page).not_to have_content("baz")
+ end
+
+ end
+
describe 'sorting by milestone' do
before :each do
foo.milestone = newer_due_milestone