Merge branch '53431-fix-upcoming-milestone-filter-for-groups' into 'master'
Add group milestones in upcoming filter Closes #53431 See merge request gitlab-org/gitlab-ce!23098
This commit is contained in:
commit
7a10ef6e75
|
@ -149,6 +149,18 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def related_groups
|
||||
if project? && project && project.group && Ability.allowed?(current_user, :read_group, project.group)
|
||||
project.group.self_and_ancestors
|
||||
elsif group
|
||||
[group]
|
||||
elsif current_user
|
||||
Gitlab::ObjectHierarchy.new(current_user.authorized_groups, current_user.groups).all_objects
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def project?
|
||||
params[:project_id].present?
|
||||
end
|
||||
|
@ -163,8 +175,10 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def projects(items = nil)
|
||||
return @projects = project if project?
|
||||
def projects
|
||||
return @projects if defined?(@projects)
|
||||
|
||||
return @projects = [project] if project?
|
||||
|
||||
projects =
|
||||
if current_user && params[:authorized_only].presence && !current_user_related?
|
||||
|
@ -459,7 +473,7 @@ class IssuableFinder
|
|||
elsif filter_by_any_milestone?
|
||||
items = items.any_milestone
|
||||
elsif filter_by_upcoming_milestone?
|
||||
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
|
||||
upcoming_ids = Milestone.upcoming_ids(projects, related_groups)
|
||||
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
|
||||
elsif filter_by_started_milestone?
|
||||
items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
|
||||
|
|
|
@ -38,12 +38,14 @@ class Milestone < ActiveRecord::Base
|
|||
scope :closed, -> { with_state(:closed) }
|
||||
scope :for_projects, -> { where(group: nil).includes(:project) }
|
||||
|
||||
scope :for_projects_and_groups, -> (project_ids, group_ids) do
|
||||
conditions = []
|
||||
conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any?
|
||||
conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any?
|
||||
scope :for_projects_and_groups, -> (projects, groups) do
|
||||
projects = projects.compact if projects.is_a? Array
|
||||
projects = [] if projects.nil?
|
||||
|
||||
where(conditions.reduce(:or))
|
||||
groups = groups.compact if groups.is_a? Array
|
||||
groups = [] if groups.nil?
|
||||
|
||||
where(project: projects).or(where(group: groups))
|
||||
end
|
||||
|
||||
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
|
||||
|
@ -133,18 +135,29 @@ class Milestone < ActiveRecord::Base
|
|||
@link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
|
||||
end
|
||||
|
||||
def self.upcoming_ids_by_projects(projects)
|
||||
rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now)
|
||||
def self.upcoming_ids(projects, groups)
|
||||
rel = unscoped
|
||||
.for_projects_and_groups(projects, groups)
|
||||
.active.where('milestones.due_date > NOW()')
|
||||
|
||||
if Gitlab::Database.postgresql?
|
||||
rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
|
||||
rel.order(:project_id, :group_id, :due_date).select('DISTINCT ON (project_id, group_id) id')
|
||||
else
|
||||
# We need to use MySQL's NULL-safe comparison operator `<=>` here
|
||||
# because one of `project_id` or `group_id` is always NULL
|
||||
join_clause = <<~HEREDOC
|
||||
LEFT OUTER JOIN milestones earlier_milestones
|
||||
ON milestones.project_id <=> earlier_milestones.project_id
|
||||
AND milestones.group_id <=> earlier_milestones.group_id
|
||||
AND milestones.due_date > earlier_milestones.due_date
|
||||
AND earlier_milestones.due_date > NOW()
|
||||
AND earlier_milestones.state = 'active'
|
||||
HEREDOC
|
||||
|
||||
rel
|
||||
.group(:project_id, :due_date, :id)
|
||||
.having('due_date = MIN(due_date)')
|
||||
.pluck(:id, :project_id, :due_date)
|
||||
.uniq(&:second)
|
||||
.map(&:first)
|
||||
.joins(join_clause)
|
||||
.where('earlier_milestones.id IS NULL')
|
||||
.select(:id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix upcoming milestones filter not including group milestones
|
||||
merge_request: 23098
|
||||
author: Heinrich Lee Yu
|
||||
type: fixed
|
|
@ -174,9 +174,13 @@ describe IssuesFinder do
|
|||
context 'filtering by upcoming milestone' do
|
||||
let(:params) { { milestone_title: Milestone::Upcoming.name } }
|
||||
|
||||
let!(:group) { create(:group, :public) }
|
||||
let!(:group_member) { create(:group_member, group: group, user: user) }
|
||||
|
||||
let(:project_no_upcoming_milestones) { create(:project, :public) }
|
||||
let(:project_next_1_1) { create(:project, :public) }
|
||||
let(:project_next_8_8) { create(:project, :public) }
|
||||
let(:project_in_group) { create(:project, :public, namespace: group) }
|
||||
|
||||
let(:yesterday) { Date.today - 1.day }
|
||||
let(:tomorrow) { Date.today + 1.day }
|
||||
|
@ -187,21 +191,22 @@ describe IssuesFinder do
|
|||
[
|
||||
create(:milestone, :closed, project: project_no_upcoming_milestones),
|
||||
create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
|
||||
create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now),
|
||||
create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday),
|
||||
create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow)
|
||||
create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now),
|
||||
create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday),
|
||||
create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow),
|
||||
create(:milestone, group: group, title: '9.9', due_date: tomorrow)
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
milestones.each do |milestone|
|
||||
create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user])
|
||||
create(:issue, project: milestone.project || project_in_group, milestone: milestone, author: user, assignees: [user])
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns issues in the upcoming milestone for each project' do
|
||||
expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8')
|
||||
expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now)
|
||||
it 'returns issues in the upcoming milestone for each project or group' do
|
||||
expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8', '9.9')
|
||||
expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now, tomorrow)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -240,7 +240,88 @@ describe Milestone do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.upcoming_ids_by_projects' do
|
||||
describe '#for_projects_and_groups' do
|
||||
let(:project) { create(:project) }
|
||||
let(:project_other) { create(:project) }
|
||||
let(:group) { create(:group) }
|
||||
let(:group_other) { create(:group) }
|
||||
|
||||
before do
|
||||
create(:milestone, project: project)
|
||||
create(:milestone, project: project_other)
|
||||
create(:milestone, group: group)
|
||||
create(:milestone, group: group_other)
|
||||
end
|
||||
|
||||
subject { described_class.for_projects_and_groups(projects, groups) }
|
||||
|
||||
shared_examples 'filters by projects and groups' do
|
||||
it 'returns milestones filtered by project' do
|
||||
milestones = described_class.for_projects_and_groups(projects, [])
|
||||
|
||||
expect(milestones.count).to eq(1)
|
||||
expect(milestones.first.project_id).to eq(project.id)
|
||||
end
|
||||
|
||||
it 'returns milestones filtered by group' do
|
||||
milestones = described_class.for_projects_and_groups([], groups)
|
||||
|
||||
expect(milestones.count).to eq(1)
|
||||
expect(milestones.first.group_id).to eq(group.id)
|
||||
end
|
||||
|
||||
it 'returns milestones filtered by both project and group' do
|
||||
milestones = described_class.for_projects_and_groups(projects, groups)
|
||||
|
||||
expect(milestones.count).to eq(2)
|
||||
expect(milestones).to contain_exactly(project.milestones.first, group.milestones.first)
|
||||
end
|
||||
end
|
||||
|
||||
context 'ids as params' do
|
||||
let(:projects) { [project.id] }
|
||||
let(:groups) { [group.id] }
|
||||
|
||||
it_behaves_like 'filters by projects and groups'
|
||||
end
|
||||
|
||||
context 'relations as params' do
|
||||
let(:projects) { Project.where(id: project.id) }
|
||||
let(:groups) { Group.where(id: group.id) }
|
||||
|
||||
it_behaves_like 'filters by projects and groups'
|
||||
end
|
||||
|
||||
context 'objects as params' do
|
||||
let(:projects) { [project] }
|
||||
let(:groups) { [group] }
|
||||
|
||||
it_behaves_like 'filters by projects and groups'
|
||||
end
|
||||
|
||||
it 'returns no records if projects and groups are nil' do
|
||||
milestones = described_class.for_projects_and_groups(nil, nil)
|
||||
|
||||
expect(milestones).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '.upcoming_ids' do
|
||||
let(:group_1) { create(:group) }
|
||||
let(:group_2) { create(:group) }
|
||||
let(:group_3) { create(:group) }
|
||||
let(:groups) { [group_1, group_2, group_3] }
|
||||
|
||||
let!(:past_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now - 1.day) }
|
||||
let!(:current_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 1.day) }
|
||||
let!(:future_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 2.days) }
|
||||
|
||||
let!(:past_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now - 1.day) }
|
||||
let!(:closed_milestone_group_2) { create(:milestone, :closed, group: group_2, due_date: Time.now + 1.day) }
|
||||
let!(:current_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now + 2.days) }
|
||||
|
||||
let!(:past_milestone_group_3) { create(:milestone, group: group_3, due_date: Time.now - 1.day) }
|
||||
|
||||
let(:project_1) { create(:project) }
|
||||
let(:project_2) { create(:project) }
|
||||
let(:project_3) { create(:project) }
|
||||
|
@ -256,16 +337,20 @@ describe Milestone do
|
|||
|
||||
let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) }
|
||||
|
||||
# The call to `#try` is because this returns a relation with a Postgres DB,
|
||||
# and an array of IDs with a MySQL DB.
|
||||
let(:milestone_ids) { described_class.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } }
|
||||
let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) }
|
||||
|
||||
it 'returns the next upcoming open milestone ID for each project' do
|
||||
expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id)
|
||||
it 'returns the next upcoming open milestone ID for each project and group' do
|
||||
expect(milestone_ids).to contain_exactly(
|
||||
current_milestone_project_1.id,
|
||||
current_milestone_project_2.id,
|
||||
current_milestone_group_1.id,
|
||||
current_milestone_group_2.id
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the projects have no open upcoming milestones' do
|
||||
context 'when the projects and groups have no open upcoming milestones' do
|
||||
let(:projects) { [project_3] }
|
||||
let(:groups) { [group_3] }
|
||||
|
||||
it 'returns no results' do
|
||||
expect(milestone_ids).to be_empty
|
||||
|
|
Loading…
Reference in New Issue