Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
cd5f3d3e
Commit
cd5f3d3e
authored
May 05, 2020
by
Jarka Košanová
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '214846-sprints-services' into 'master'
Create SprintsFinder See merge request gitlab-org/gitlab!29822
parents
55528807
6abaff70
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
250 additions
and
60 deletions
+250
-60
app/models/concerns/timebox.rb
app/models/concerns/timebox.rb
+66
-0
app/models/milestone.rb
app/models/milestone.rb
+0
-59
ee/app/finders/sprints_finder.rb
ee/app/finders/sprints_finder.rb
+65
-0
ee/spec/finders/sprints_finder_spec.rb
ee/spec/finders/sprints_finder_spec.rb
+118
-0
spec/models/milestone_spec.rb
spec/models/milestone_spec.rb
+1
-1
No files found.
app/models/concerns/timebox.rb
View file @
cd5f3d3e
...
...
@@ -5,10 +5,31 @@ module Timebox
include
AtomicInternalId
include
CacheMarkdownField
include
Gitlab
::
SQL
::
Pattern
include
IidRoutes
include
StripAttribute
TimeboxStruct
=
Struct
.
new
(
:title
,
:name
,
:id
)
do
# Ensure these models match the interface required for exporting
def
serializable_hash
(
_opts
=
{})
{
title:
title
,
name:
name
,
id:
id
}
end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
# Requests that have no timeboxes assigned.
None
=
TimeboxStruct
.
new
(
'No Timebox'
,
'No Timebox'
,
0
)
Any
=
TimeboxStruct
.
new
(
'Any Timebox'
,
''
,
-
1
)
Upcoming
=
TimeboxStruct
.
new
(
'Upcoming'
,
'#upcoming'
,
-
2
)
Started
=
TimeboxStruct
.
new
(
'Started'
,
'#started'
,
-
3
)
included
do
# Defines the same constants above, but inside the including class.
const_set
:None
,
TimeboxStruct
.
new
(
"No
#{
self
.
name
}
"
,
"No
#{
self
.
name
}
"
,
0
)
const_set
:Any
,
TimeboxStruct
.
new
(
"Any
#{
self
.
name
}
"
,
''
,
-
1
)
const_set
:Upcoming
,
TimeboxStruct
.
new
(
'Upcoming'
,
'#upcoming'
,
-
2
)
const_set
:Started
,
TimeboxStruct
.
new
(
'Started'
,
'#started'
,
-
3
)
alias_method
:timebox_id
,
:id
validates
:group
,
presence:
true
,
unless: :project
...
...
@@ -35,6 +56,7 @@ module Timebox
scope
:active
,
->
{
with_state
(
:active
)
}
scope
:closed
,
->
{
with_state
(
:closed
)
}
scope
:for_projects
,
->
{
where
(
group:
nil
).
includes
(
:project
)
}
scope
:with_title
,
->
(
title
)
{
where
(
title:
title
)
}
scope
:for_projects_and_groups
,
->
(
projects
,
groups
)
do
projects
=
projects
.
compact
if
projects
.
is_a?
Array
...
...
@@ -57,6 +79,50 @@ module Timebox
alias_attribute
:name
,
:title
end
class_methods
do
# Searches for timeboxes with a matching title or description.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def
search
(
query
)
fuzzy_search
(
query
,
[
:title
,
:description
])
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def
search_title
(
query
)
fuzzy_search
(
query
,
[
:title
])
end
def
filter_by_state
(
timeboxes
,
state
)
case
state
when
'closed'
then
timeboxes
.
closed
when
'all'
then
timeboxes
else
timeboxes
.
active
end
end
def
count_by_state
reorder
(
nil
).
group
(
:state
).
count
end
def
predefined_id?
(
id
)
[
Any
.
id
,
None
.
id
,
Upcoming
.
id
,
Started
.
id
].
include?
(
id
)
end
def
predefined?
(
timebox
)
predefined_id?
(
timebox
&
.
id
)
end
end
def
title
=
(
value
)
write_attribute
(
:title
,
sanitize_title
(
value
))
if
value
.
present?
end
...
...
app/models/milestone.rb
View file @
cd5f3d3e
# frozen_string_literal: true
class
Milestone
<
ApplicationRecord
# Represents a "No Milestone" state used for filtering Issues and Merge
# Requests that have no milestone assigned.
MilestoneStruct
=
Struct
.
new
(
:title
,
:name
,
:id
)
do
# Ensure these models match the interface required for exporting
def
serializable_hash
(
_opts
=
{})
{
title:
title
,
name:
name
,
id:
id
}
end
end
None
=
MilestoneStruct
.
new
(
'No Milestone'
,
'No Milestone'
,
0
)
Any
=
MilestoneStruct
.
new
(
'Any Milestone'
,
''
,
-
1
)
Upcoming
=
MilestoneStruct
.
new
(
'Upcoming'
,
'#upcoming'
,
-
2
)
Started
=
MilestoneStruct
.
new
(
'Started'
,
'#started'
,
-
3
)
include
Sortable
include
Referable
include
Timebox
include
Milestoneish
include
FromUnion
include
Importable
include
Gitlab
::
SQL
::
Pattern
prepend_if_ee
(
'::EE::Milestone'
)
# rubocop: disable Cop/InjectEnterpriseEditionModule
...
...
@@ -54,50 +39,6 @@ class Milestone < ApplicationRecord
state
:active
end
class
<<
self
# Searches for milestones with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def
search
(
query
)
fuzzy_search
(
query
,
[
:title
,
:description
])
end
# Searches for milestones with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def
search_title
(
query
)
fuzzy_search
(
query
,
[
:title
])
end
def
filter_by_state
(
milestones
,
state
)
case
state
when
'closed'
then
milestones
.
closed
when
'all'
then
milestones
else
milestones
.
active
end
end
def
count_by_state
reorder
(
nil
).
group
(
:state
).
count
end
def
predefined_id?
(
id
)
[
Any
.
id
,
None
.
id
,
Upcoming
.
id
,
Started
.
id
].
include?
(
id
)
end
def
predefined?
(
milestone
)
predefined_id?
(
milestone
&
.
id
)
end
end
def
self
.
reference_prefix
'%'
end
...
...
ee/app/finders/sprints_finder.rb
0 → 100644
View file @
cd5f3d3e
# frozen_string_literal: true
# Search for sprints
#
# params - Hash
# project_ids: Array of project ids or single project id or ActiveRecord relation.
# group_ids: Array of group ids or single group id or ActiveRecord relation.
# order - Orders by field default due date asc.
# title - Filter by title.
# state - Filters by state.
class
SprintsFinder
include
FinderMethods
include
TimeFrameFilter
attr_reader
:params
def
initialize
(
params
=
{})
@params
=
params
end
def
execute
items
=
Sprint
.
all
items
=
by_groups_and_projects
(
items
)
items
=
by_title
(
items
)
items
=
by_search_title
(
items
)
items
=
by_state
(
items
)
items
=
by_timeframe
(
items
)
order
(
items
)
end
private
def
by_groups_and_projects
(
items
)
items
.
for_projects_and_groups
(
params
[
:project_ids
],
params
[
:group_ids
])
end
def
by_title
(
items
)
if
params
[
:title
]
items
.
with_title
(
params
[
:title
])
else
items
end
end
def
by_search_title
(
items
)
if
params
[
:search_title
].
present?
items
.
search_title
(
params
[
:search_title
])
else
items
end
end
def
by_state
(
items
)
Sprint
.
filter_by_state
(
items
,
params
[
:state
])
end
# rubocop: disable CodeReuse/ActiveRecord
def
order
(
items
)
order_statement
=
Gitlab
::
Database
.
nulls_last_order
(
'due_date'
,
'ASC'
)
items
.
reorder
(
order_statement
).
order
(
:title
)
end
# rubocop: enable CodeReuse/ActiveRecord
end
ee/spec/finders/sprints_finder_spec.rb
0 → 100644
View file @
cd5f3d3e
# frozen_string_literal: true
require
'spec_helper'
describe
SprintsFinder
do
let
(
:now
)
{
Time
.
now
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:project_1
)
{
create
(
:project
,
namespace:
group
)
}
let_it_be
(
:project_2
)
{
create
(
:project
,
namespace:
group
)
}
let!
(
:started_group_sprint
)
{
create
(
:sprint
,
group:
group
,
title:
'one test'
,
start_date:
now
-
1
.
day
,
due_date:
now
)
}
let!
(
:upcoming_group_sprint
)
{
create
(
:sprint
,
group:
group
,
start_date:
now
+
1
.
day
,
due_date:
now
+
2
.
days
)
}
let!
(
:sprint_from_project_1
)
{
create
(
:sprint
,
project:
project_1
,
state:
::
Sprint
::
STATE_ID_MAP
[
:active
],
start_date:
now
+
2
.
days
,
due_date:
now
+
3
.
days
)
}
let!
(
:sprint_from_project_2
)
{
create
(
:sprint
,
project:
project_2
,
state:
::
Sprint
::
STATE_ID_MAP
[
:active
],
start_date:
now
+
4
.
days
,
due_date:
now
+
5
.
days
)
}
let
(
:project_ids
)
{
[
project_1
.
id
,
project_2
.
id
]
}
subject
{
described_class
.
new
(
params
).
execute
}
context
'sprints for projects'
do
let
(
:params
)
{
{
project_ids:
project_ids
,
state:
'all'
}
}
it
'returns sprints for projects'
do
expect
(
subject
).
to
contain_exactly
(
sprint_from_project_1
,
sprint_from_project_2
)
end
end
context
'sprints for groups'
do
let
(
:params
)
{
{
group_ids:
group
.
id
,
state:
'all'
}
}
it
'returns sprints for groups'
do
expect
(
subject
).
to
contain_exactly
(
started_group_sprint
,
upcoming_group_sprint
)
end
end
context
'sprints for groups and project'
do
let
(
:params
)
{
{
project_ids:
project_ids
,
group_ids:
group
.
id
,
state:
'all'
}
}
it
'returns sprints for groups and projects'
do
expect
(
subject
).
to
contain_exactly
(
started_group_sprint
,
upcoming_group_sprint
,
sprint_from_project_1
,
sprint_from_project_2
)
end
it
'orders sprints by due date'
do
sprint
=
create
(
:sprint
,
group:
group
,
due_date:
now
-
2
.
days
)
expect
(
subject
.
first
).
to
eq
(
sprint
)
expect
(
subject
.
second
).
to
eq
(
started_group_sprint
)
expect
(
subject
.
third
).
to
eq
(
upcoming_group_sprint
)
end
end
context
'with filters'
do
let
(
:params
)
do
{
project_ids:
project_ids
,
group_ids:
group
.
id
,
state:
'all'
}
end
before
do
started_group_sprint
.
close
sprint_from_project_1
.
close
end
it
'filters by active state'
do
params
[
:state
]
=
'active'
expect
(
subject
).
to
contain_exactly
(
upcoming_group_sprint
,
sprint_from_project_2
)
end
it
'filters by closed state'
do
params
[
:state
]
=
'closed'
expect
(
subject
).
to
contain_exactly
(
started_group_sprint
,
sprint_from_project_1
)
end
it
'filters by title'
do
params
[
:title
]
=
'one test'
expect
(
subject
.
to_a
).
to
contain_exactly
(
started_group_sprint
)
end
it
'filters by search_title'
do
params
[
:search_title
]
=
'one t'
expect
(
subject
.
to_a
).
to
contain_exactly
(
started_group_sprint
)
end
context
'by timeframe'
do
it
'returns sprints with start_date and due_date between timeframe'
do
params
.
merge!
(
start_date:
now
-
1
.
day
,
end_date:
now
+
3
.
days
)
expect
(
subject
).
to
match_array
([
started_group_sprint
,
upcoming_group_sprint
,
sprint_from_project_1
])
end
it
'returns sprints which start before the timeframe'
do
sprint
=
create
(
:sprint
,
project:
project_2
,
start_date:
now
-
5
.
days
)
params
.
merge!
(
start_date:
now
-
3
.
days
,
end_date:
now
-
2
.
days
)
expect
(
subject
).
to
match_array
([
sprint
])
end
it
'returns sprints which end after the timeframe'
do
sprint
=
create
(
:sprint
,
project:
project_2
,
due_date:
now
+
6
.
days
)
params
.
merge!
(
start_date:
now
+
6
.
days
,
end_date:
now
+
7
.
days
)
expect
(
subject
).
to
match_array
([
sprint
])
end
end
end
describe
'#find_by'
do
it
'finds a single sprint'
do
finder
=
described_class
.
new
(
project_ids:
[
project_1
.
id
],
state:
'all'
)
expect
(
finder
.
find_by
(
iid:
sprint_from_project_1
.
iid
)).
to
eq
(
sprint_from_project_1
)
end
end
end
spec/models/milestone_spec.rb
View file @
cd5f3d3e
...
...
@@ -6,7 +6,7 @@ describe Milestone do
it_behaves_like
'a timebox'
,
:milestone
describe
'MilestoneStruct#serializable_hash'
do
let
(
:predefined_milestone
)
{
described_class
::
Milestone
Struct
.
new
(
'Test Milestone'
,
'#test'
,
1
)
}
let
(
:predefined_milestone
)
{
described_class
::
Timebox
Struct
.
new
(
'Test Milestone'
,
'#test'
,
1
)
}
it
'presents the predefined milestone as a hash'
do
expect
(
predefined_milestone
.
serializable_hash
).
to
eq
(
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment