Commit 3afd0817 authored by Mehmet Beydogan's avatar Mehmet Beydogan Committed by Robert Speicher

Add due_date:time field to Issue model

Add due_date text field to sidebar issue#show
Add ability sorting issues by due date ASC and DESC
Add ability to filtering issues by No Due Date, Any Due Date, Due to tomorrow, Due in this week options
Add handling issue due_date field for MergeRequest
Update CHANGELOG
Fix ambigous match for issues#show sidebar
Fix SCREAMING_SNAKE_CASE offenses for due date contants
Add specs for due date sorting and filtering on issues
parent 1e596fef
...@@ -267,6 +267,7 @@ v 8.5.7 ...@@ -267,6 +267,7 @@ v 8.5.7
v 8.5.6 v 8.5.6
- Obtain a lease before querying LDAP - Obtain a lease before querying LDAP
- Add ability set due date to issues, sort and filter issues by due date
v 8.5.5 v 8.5.5
- Ensure removing a project removes associated Todo entries - Ensure removing a project removes associated Todo entries
......
...@@ -192,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -192,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential, :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 end
......
...@@ -39,6 +39,7 @@ class IssuableFinder ...@@ -39,6 +39,7 @@ class IssuableFinder
items = by_assignee(items) items = by_assignee(items)
items = by_author(items) items = by_author(items)
items = by_label(items) items = by_label(items)
items = by_due_date(items)
sort(items) sort(items)
end end
...@@ -112,6 +113,14 @@ class IssuableFinder ...@@ -112,6 +113,14 @@ class IssuableFinder
end end
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? def labels?
params[:label_name].present? params[:label_name].present?
end end
...@@ -283,6 +292,19 @@ class IssuableFinder ...@@ -283,6 +292,19 @@ class IssuableFinder
items.distinct items.distinct
end 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 def label_names
params[:label_name].split(',') params[:label_name].split(',')
end end
......
...@@ -172,6 +172,17 @@ module IssuesHelper ...@@ -172,6 +172,17 @@ module IssuesHelper
end.to_h end.to_h
end 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 # Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end
...@@ -8,6 +8,8 @@ module SortingHelper ...@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created => sort_title_oldest_created, sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later, 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_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin, sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin, sort_value_oldest_signin => sort_title_oldest_signin,
...@@ -50,6 +52,14 @@ module SortingHelper ...@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later' 'Milestone due later'
end 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 def sort_title_name
'Name' 'Name'
end end
...@@ -98,6 +108,14 @@ module SortingHelper ...@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc' 'milestone_due_desc'
end 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 def sort_value_name
'name_asc' 'name_asc'
end end
......
...@@ -39,6 +39,8 @@ module Issuable ...@@ -39,6 +39,8 @@ module Issuable
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } 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 :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 :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 :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) } scope :references_project, -> { references(:project) }
......
...@@ -18,6 +18,8 @@ module Sortable ...@@ -18,6 +18,8 @@ module Sortable
scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) } 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 end
module ClassMethods module ClassMethods
...@@ -31,6 +33,8 @@ module Sortable ...@@ -31,6 +33,8 @@ module Sortable
when 'created_desc' then order_created_desc when 'created_desc' then order_created_desc
when 'id_desc' then order_id_desc when 'id_desc' then order_id_desc
when 'id_asc' then order_id_asc when 'id_asc' then order_id_asc
when 'due_date_asc' then due_date_asc
when 'due_date_desc' then due_date_desc
else else
all all
end end
......
...@@ -28,6 +28,9 @@ class Issue < ActiveRecord::Base ...@@ -28,6 +28,9 @@ class Issue < ActiveRecord::Base
include Sortable include Sortable
include Taskable include Taskable
NO_DUE_DATE = ['No Due Date', '0']
ANY_DUE_DATE = ['Any Due Date', '']
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
belongs_to :project belongs_to :project
......
...@@ -48,6 +48,10 @@ ...@@ -48,6 +48,10 @@
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o') = icon('clock-o')
= issue.milestone.title = issue.milestone.title
- if issue.due_date
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
- if issue.labels.any? - if issue.labels.any?
&nbsp; &nbsp;
- issue.labels.each do |label| - issue.labels.each do |label|
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
= sort_title_milestone_soon = sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do = link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later = 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 = link_to page_filter_path(sort: sort_value_upvotes) do
= sort_title_upvotes = sort_title_upvotes
= link_to page_filter_path(sort: sort_value_downvotes) do = link_to page_filter_path(sort: sort_value_downvotes) do
......
...@@ -23,6 +23,13 @@ ...@@ -23,6 +23,13 @@
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown" = 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 .pull-right
= render 'shared/sort_dropdown' = render 'shared/sort_dropdown'
......
...@@ -74,6 +74,31 @@ ...@@ -74,6 +74,31 @@
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = 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 }}) = 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? - if issuable.project.labels.any?
.block.labels .block.labels
......
class AddDueDateToIssues < ActiveRecord::Migration
def change
add_column :issues, :due_date, :date
end
end
...@@ -366,6 +366,19 @@ ActiveRecord::Schema.define(version: 20160419120017) do ...@@ -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", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", 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| create_table "events", force: :cascade do |t|
t.string "target_type" t.string "target_type"
t.integer "target_id" t.integer "target_id"
...@@ -422,6 +435,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do ...@@ -422,6 +435,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
t.integer "moved_to_id" t.integer "moved_to_id"
t.boolean "confidential", default: false t.boolean "confidential", default: false
t.datetime "deleted_at" t.datetime "deleted_at"
t.date "due_date"
end end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
...@@ -431,6 +445,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do ...@@ -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", ["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", ["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", ["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", ["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", "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 add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
......
...@@ -153,6 +153,69 @@ describe 'Issues', feature: true do ...@@ -153,6 +153,69 @@ describe 'Issues', feature: true do
expect(first_issue).to include('baz') expect(first_issue).to include('baz')
end 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 describe 'sorting by milestone' do
before :each do before :each do
foo.milestone = newer_due_milestone foo.milestone = newer_due_milestone
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment