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
This commit is contained in:
parent
1e596fef1c
commit
3afd08170d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
= icon('calendar')
|
||||
= issue.due_date.to_s(:medium)
|
||||
- if issue.labels.any?
|
||||
|
||||
- issue.labels.each do |label|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddDueDateToIssues < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :issues, :due_date, :date
|
||||
end
|
||||
end
|
15
db/schema.rb
15
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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue