From 3afd08170da6f003b4f11ae1a3f737b14dedec40 Mon Sep 17 00:00:00 2001 From: Mehmet Beydogan <mehmet.beydogan@gmail.com> Date: Thu, 10 Mar 2016 16:26:56 +0200 Subject: [PATCH] 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 --- CHANGELOG | 1 + app/controllers/projects/issues_controller.rb | 2 +- app/finders/issuable_finder.rb | 22 +++++++ app/helpers/issues_helper.rb | 11 ++++ app/helpers/sorting_helper.rb | 18 ++++++ app/models/concerns/issuable.rb | 2 + app/models/concerns/sortable.rb | 4 ++ app/models/issue.rb | 3 + app/views/projects/issues/_issue.html.haml | 4 ++ app/views/shared/_sort_dropdown.html.haml | 4 ++ app/views/shared/issuable/_filter.html.haml | 7 +++ app/views/shared/issuable/_sidebar.html.haml | 25 ++++++++ .../20160310124959_add_due_date_to_issues.rb | 5 ++ db/schema.rb | 15 +++++ spec/features/issues_spec.rb | 63 +++++++++++++++++++ 15 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160310124959_add_due_date_to_issues.rb 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 + + = icon('calendar') + = issue.due_date.to_s(:medium) - if issue.labels.any? - 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 -- GitLab