Merge branch 'group_milestones' into 'master'
Group milestones
This commit is contained in:
commit
8fb76a8308
17 changed files with 547 additions and 0 deletions
|
@ -7,3 +7,7 @@
|
|||
.member-search-form {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.milestone-row {
|
||||
@include str-truncated(90%);
|
||||
}
|
||||
|
|
54
app/controllers/groups/milestones_controller.rb
Normal file
54
app/controllers/groups/milestones_controller.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
class Groups::MilestonesController < ApplicationController
|
||||
layout 'group'
|
||||
|
||||
before_filter :authorize_group_milestone!, only: :update
|
||||
|
||||
def index
|
||||
project_milestones = Milestone.where(project_id: group.projects)
|
||||
@group_milestones = Milestones::GroupService.new(project_milestones).execute
|
||||
@group_milestones = case params[:status]
|
||||
when 'all'; @group_milestones
|
||||
when 'closed'; status('closed')
|
||||
else status('active')
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
project_milestones = Milestone.where(project_id: group.projects)
|
||||
@group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
|
||||
end
|
||||
|
||||
def update
|
||||
project_milestones = Milestone.where(project_id: group.projects)
|
||||
@group_milestones = Milestones::GroupService.new(project_milestones).milestone(title)
|
||||
|
||||
@group_milestones.milestones.each do |milestone|
|
||||
Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.html do
|
||||
redirect_to group_milestones_path(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(path: params[:group_id])
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def status(state)
|
||||
@group_milestones.map{ |milestone| next if milestone.state != state; milestone }.compact
|
||||
end
|
||||
|
||||
def authorize_group_milestone!
|
||||
return render_404 unless can?(current_user, :manage_group, group)
|
||||
end
|
||||
end
|
|
@ -31,6 +31,17 @@ module GroupsHelper
|
|||
end
|
||||
|
||||
title
|
||||
end
|
||||
|
||||
def group_filter_path(entity, options={})
|
||||
exist_opts = {
|
||||
status: params[:status]
|
||||
}
|
||||
|
||||
options = exist_opts.merge(options)
|
||||
|
||||
path = request.path
|
||||
path << "?#{options.to_param}"
|
||||
path
|
||||
end
|
||||
end
|
||||
|
|
95
app/models/group_milestone.rb
Normal file
95
app/models/group_milestone.rb
Normal file
|
@ -0,0 +1,95 @@
|
|||
class GroupMilestone
|
||||
|
||||
def initialize(title, milestones)
|
||||
@title = title
|
||||
@milestones = milestones
|
||||
end
|
||||
|
||||
def title
|
||||
@title
|
||||
end
|
||||
|
||||
def safe_title
|
||||
@title.parameterize
|
||||
end
|
||||
|
||||
def milestones
|
||||
@milestones
|
||||
end
|
||||
|
||||
def projects
|
||||
milestones.map { |milestone| milestone.project }
|
||||
end
|
||||
|
||||
def issue_count
|
||||
milestones.map { |milestone| milestone.issues.count }.sum
|
||||
end
|
||||
|
||||
def merge_requests_count
|
||||
milestones.map { |milestone| milestone.merge_requests.count }.sum
|
||||
end
|
||||
|
||||
def open_items_count
|
||||
milestones.map { |milestone| milestone.open_items_count }.sum
|
||||
end
|
||||
|
||||
def closed_items_count
|
||||
milestones.map { |milestone| milestone.closed_items_count }.sum
|
||||
end
|
||||
|
||||
def total_items_count
|
||||
milestones.map { |milestone| milestone.total_items_count }.sum
|
||||
end
|
||||
|
||||
def percent_complete
|
||||
((closed_items_count * 100) / total_items_count).abs
|
||||
rescue ZeroDivisionError
|
||||
100
|
||||
end
|
||||
|
||||
def state
|
||||
state = milestones.map { |milestone| milestone.state }
|
||||
|
||||
if state.count('active') == state.size
|
||||
'active'
|
||||
else
|
||||
'closed'
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
state == 'active'
|
||||
end
|
||||
|
||||
def closed?
|
||||
state == 'closed'
|
||||
end
|
||||
|
||||
def issues
|
||||
@group_issues ||= milestones.map { |milestone| milestone.issues }.flatten.group_by(&:state)
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
@group_merge_requests ||= milestones.map { |milestone| milestone.merge_requests }.flatten.group_by(&:state)
|
||||
end
|
||||
|
||||
def participants
|
||||
milestones.map { |milestone| milestone.participants.uniq }.reject(&:empty?).flatten
|
||||
end
|
||||
|
||||
def opened_issues
|
||||
issues.values_at("opened", "reopened").compact.flatten
|
||||
end
|
||||
|
||||
def closed_issues
|
||||
issues['closed']
|
||||
end
|
||||
|
||||
def opened_merge_requests
|
||||
merge_requests.values_at("opened", "reopened").compact.flatten
|
||||
end
|
||||
|
||||
def closed_merge_requests
|
||||
merge_requests.values_at("closed", "merged", "locked").compact.flatten
|
||||
end
|
||||
end
|
26
app/services/milestones/group_service.rb
Normal file
26
app/services/milestones/group_service.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
module Milestones
|
||||
class GroupService < Milestones::BaseService
|
||||
def initialize(project_milestones)
|
||||
@project_milestones = project_milestones.group_by(&:title)
|
||||
end
|
||||
|
||||
def execute
|
||||
build(@project_milestones)
|
||||
end
|
||||
|
||||
def milestone(title)
|
||||
if title
|
||||
group_milestone = @project_milestones[title].group_by(&:title)
|
||||
build(group_milestone).first
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build(milestone)
|
||||
milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) }
|
||||
end
|
||||
end
|
||||
end
|
12
app/views/groups/_filter.html.haml
Normal file
12
app/views/groups/_filter.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
= form_tag group_filter_path(entity), method: 'get' do
|
||||
%fieldset
|
||||
%ul.nav.nav-pills.nav-stacked
|
||||
%li{class: ("active" if (params[:status] == 'active' || !params[:status]))}
|
||||
= link_to group_filter_path(entity, status: 'active') do
|
||||
Active
|
||||
%li{class: ("active" if params[:status] == 'closed')}
|
||||
= link_to group_filter_path(entity, status: 'closed') do
|
||||
Closed
|
||||
%li{class: ("active" if params[:status] == 'all')}
|
||||
= link_to group_filter_path(entity, status: 'all') do
|
||||
All
|
10
app/views/groups/milestones/_issue.html.haml
Normal file
10
app/views/groups/milestones/_issue.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
|
||||
%span.milestone-row
|
||||
- project = issue.project
|
||||
%strong #{project.name} ·
|
||||
= link_to [project, issue] do
|
||||
%span.cgray ##{issue.iid}
|
||||
= link_to_gfm issue.title, [project, issue]
|
||||
.pull-right.assignee-icon
|
||||
- if issue.assignee
|
||||
= image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16"
|
6
app/views/groups/milestones/_issues.html.haml
Normal file
6
app/views/groups/milestones/_issues.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
.panel.panel-default
|
||||
.panel-heading= title
|
||||
%ul{ class: "well-list issues-sortable-list" }
|
||||
- if issues
|
||||
- issues.each do |issue|
|
||||
= render 'issue', issue: issue
|
10
app/views/groups/milestones/_merge_request.html.haml
Normal file
10
app/views/groups/milestones/_merge_request.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
|
||||
%span.milestone-row
|
||||
- project = merge_request.project
|
||||
%strong #{project.name} ·
|
||||
= link_to [project, merge_request] do
|
||||
%span.cgray ##{merge_request.iid}
|
||||
= link_to_gfm merge_request.title, [project, merge_request]
|
||||
.pull-right.assignee-icon
|
||||
- if merge_request.assignee
|
||||
= image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16"
|
6
app/views/groups/milestones/_merge_requests.html.haml
Normal file
6
app/views/groups/milestones/_merge_requests.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
.panel.panel-default
|
||||
.panel-heading= title
|
||||
%ul{ class: "well-list merge_requests-sortable-list" }
|
||||
- if merge_requests
|
||||
- merge_requests.each do |merge_request|
|
||||
= render 'merge_request', merge_request: merge_request
|
49
app/views/groups/milestones/index.html.haml
Normal file
49
app/views/groups/milestones/index.html.haml
Normal file
|
@ -0,0 +1,49 @@
|
|||
%h3.page-title
|
||||
Milestones
|
||||
%span.pull-right #{@group_milestones.count} milestones
|
||||
|
||||
%p.light
|
||||
Only milestones from
|
||||
%strong #{@group.name}
|
||||
group are listed here.
|
||||
|
||||
%hr
|
||||
|
||||
.row
|
||||
.fixed.sidebar-expand-button.hidden-lg.hidden-md
|
||||
%i.icon-list.icon-2x
|
||||
.col-md-3.responsive-side
|
||||
= render 'groups/filter', entity: 'milestone'
|
||||
.col-md-9
|
||||
.panel.panel-default
|
||||
%ul.well-list
|
||||
- if @group_milestones.blank?
|
||||
%li
|
||||
.nothing-here-block No milestones to show
|
||||
- else
|
||||
- @group_milestones.each do |milestone|
|
||||
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
|
||||
.pull-right
|
||||
- if can?(current_user, :manage_group, @group)
|
||||
- if milestone.closed?
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped"
|
||||
- else
|
||||
= link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-remove"
|
||||
%h4
|
||||
= link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
|
||||
%div
|
||||
%div
|
||||
= link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
|
||||
= pluralize milestone.issue_count, 'Issue'
|
||||
|
||||
= link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
|
||||
= pluralize milestone.merge_requests_count, 'Merge Request'
|
||||
|
||||
%span.light #{milestone.percent_complete}% complete
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{milestone.percent_complete}%;"}
|
||||
%div
|
||||
%br
|
||||
- milestone.projects.each do |project|
|
||||
%span.label.label-default
|
||||
= project.name
|
76
app/views/groups/milestones/show.html.haml
Normal file
76
app/views/groups/milestones/show.html.haml
Normal file
|
@ -0,0 +1,76 @@
|
|||
%h3.page-title
|
||||
Milestone #{@group_milestone.title}
|
||||
.pull-right
|
||||
- if can?(current_user, :manage_group, @group)
|
||||
- if @group_milestone.active?
|
||||
= link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-remove"
|
||||
- else
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped"
|
||||
|
||||
- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
|
||||
.alert.alert-success
|
||||
%span All issues for this milestone are closed. You may close the milestone now.
|
||||
|
||||
.back-link
|
||||
= link_to group_milestones_path(@group) do
|
||||
← To milestones list
|
||||
|
||||
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
|
||||
.state.clearfix
|
||||
.state-label
|
||||
- if @group_milestone.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
|
||||
%h4.title
|
||||
= gfm escape_once(@group_milestone.title)
|
||||
|
||||
.context
|
||||
%p
|
||||
Progress:
|
||||
#{@group_milestone.closed_items_count} closed
|
||||
–
|
||||
#{@group_milestone.open_items_count} open
|
||||
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{@group_milestone.percent_complete}%;"}
|
||||
|
||||
%ul.nav.nav-tabs
|
||||
%li.active
|
||||
= link_to '#tab-issues', 'data-toggle' => 'tab' do
|
||||
Issues
|
||||
%span.badge= @group_milestone.issue_count
|
||||
%li
|
||||
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
|
||||
Merge Requests
|
||||
%span.badge= @group_milestone.merge_requests_count
|
||||
%li
|
||||
= link_to '#tab-participants', 'data-toggle' => 'tab' do
|
||||
Participants
|
||||
%span.badge= @group_milestone.participants.count
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#tab-issues
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'issues', title: "Open", issues: @group_milestone.opened_issues
|
||||
.col-md-6
|
||||
= render 'issues', title: "Closed", issues: @group_milestone.closed_issues
|
||||
|
||||
.tab-pane#tab-merge-requests
|
||||
.row
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests
|
||||
.col-md-6
|
||||
= render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests
|
||||
|
||||
.tab-pane#tab-participants
|
||||
%ul.bordered-list
|
||||
- @group_milestone.participants.each do |user|
|
||||
%li
|
||||
= link_to user, title: user.name, class: "darken" do
|
||||
= image_tag avatar_icon(user.email, 32), class: "avatar s32"
|
||||
%strong= truncate(user.name, lenght: 40)
|
||||
%br
|
||||
%small.cgray= user.username
|
|
@ -2,6 +2,9 @@
|
|||
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
|
||||
= link_to group_path(@group), title: "Home" do
|
||||
Activity
|
||||
= nav_link(path: 'groups#milestones') do
|
||||
= link_to group_milestones_path(@group) do
|
||||
Milestones
|
||||
= nav_link(path: 'groups#issues') do
|
||||
= link_to issues_group_path(@group) do
|
||||
Issues
|
||||
|
|
|
@ -151,8 +151,10 @@ Gitlab::Application.routes.draw do
|
|||
end
|
||||
|
||||
resources :users_groups, only: [:create, :update, :destroy]
|
||||
|
||||
scope module: :groups do
|
||||
resource :avatar, only: [:destroy]
|
||||
resources :milestones
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -120,3 +120,22 @@ Feature: Groups
|
|||
When I search for 'Mary' member
|
||||
Then I should see user "Mary Jane" in team list
|
||||
Then I should not see user "John Doe" in team list
|
||||
|
||||
|
||||
Scenario: I should see group "Owned" milestone index page with no milestones
|
||||
When I visit group "Owned" page
|
||||
And I click on group milestones
|
||||
Then I should see group milestones index page has no milestones
|
||||
|
||||
Scenario: I should see group "Owned" milestone index page with milestones
|
||||
Given Group has projects with milestones
|
||||
When I visit group "Owned" page
|
||||
And I click on group milestones
|
||||
Then I should see group milestones index page with milestones
|
||||
|
||||
Scenario: I should see group "Owned" milestone show page
|
||||
Given Group has projects with milestones
|
||||
When I visit group "Owned" page
|
||||
And I click on group milestones
|
||||
And I click on one group milestone
|
||||
Then I should see group milestone with all issues and MRs assigned to that milestone
|
||||
|
|
|
@ -164,6 +164,36 @@ class Groups < Spinach::FeatureSteps
|
|||
end
|
||||
end
|
||||
|
||||
step 'I click on group milestones' do
|
||||
click_link 'Milestones'
|
||||
end
|
||||
|
||||
step 'I should see group milestones index page has no milestones' do
|
||||
page.should have_content('No milestones to show')
|
||||
end
|
||||
|
||||
step 'Group has projects with milestones' do
|
||||
group_milestone
|
||||
end
|
||||
|
||||
step 'I should see group milestones index page with milestones' do
|
||||
page.should have_content('Version 7.2')
|
||||
page.should have_content('GL-113')
|
||||
page.should have_link('2 Issues', href: group_milestone_path("owned", "version-7-2", title: "Version 7.2"))
|
||||
page.should have_link('3 Merge Requests', href: group_milestone_path("owned", "gl-113", title: "GL-113"))
|
||||
end
|
||||
|
||||
step 'I click on one group milestone' do
|
||||
click_link 'GL-113'
|
||||
end
|
||||
|
||||
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
|
||||
page.should have_content('Milestone GL-113')
|
||||
page.should have_content('Progress: 0 closed – 4 open')
|
||||
page.should have_link(@issue1.title, href: project_issue_path(@project1, @issue1))
|
||||
page.should have_link(@mr3.title, href: project_merge_request_path(@project3, @mr3))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def assigned_to_me key
|
||||
|
@ -173,4 +203,68 @@ class Groups < Spinach::FeatureSteps
|
|||
def project
|
||||
Group.find_by(name: "Owned").projects.first
|
||||
end
|
||||
|
||||
def group_milestone
|
||||
group = Group.find_by(name: "Owned")
|
||||
|
||||
@project1 = create :project,
|
||||
group: group
|
||||
project2 = create :project,
|
||||
path: 'gitlab-ci',
|
||||
group: group
|
||||
@project3 = create :project,
|
||||
path: 'cookbook-gitlab',
|
||||
group: group
|
||||
milestone1_project1 = create :milestone,
|
||||
title: "Version 7.2",
|
||||
project: @project1
|
||||
milestone1_project2 = create :milestone,
|
||||
title: "Version 7.2",
|
||||
project: project2
|
||||
milestone1_project3 = create :milestone,
|
||||
title: "Version 7.2",
|
||||
project: @project3
|
||||
milestone2_project1 = create :milestone,
|
||||
title: "GL-113",
|
||||
project: @project1
|
||||
milestone2_project2 = create :milestone,
|
||||
title: "GL-113",
|
||||
project: project2
|
||||
milestone2_project3 = create :milestone,
|
||||
title: "GL-113",
|
||||
project: @project3
|
||||
@issue1 = create :issue,
|
||||
project: @project1,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone2_project1
|
||||
issue2 = create :issue,
|
||||
project: project2,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone1_project2
|
||||
issue3 = create :issue,
|
||||
project: @project3,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone1_project1
|
||||
mr1 = create :merge_request,
|
||||
source_project: @project1,
|
||||
target_project: @project1,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone2_project1
|
||||
mr2 = create :merge_request,
|
||||
source_project: project2,
|
||||
target_project: project2,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone2_project2
|
||||
@mr3 = create :merge_request,
|
||||
source_project: @project3,
|
||||
target_project: @project3,
|
||||
assignee: current_user,
|
||||
author: current_user,
|
||||
milestone: milestone2_project3
|
||||
end
|
||||
end
|
||||
|
|
70
spec/services/milestones/group_service_spec.rb
Normal file
70
spec/services/milestones/group_service_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Milestones::GroupService do
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:project1) { create(:project, group: group) }
|
||||
let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
|
||||
let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
|
||||
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
|
||||
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
|
||||
let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
|
||||
let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
|
||||
let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
|
||||
let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
|
||||
|
||||
describe 'execute' do
|
||||
context 'with valid projects' do
|
||||
before do
|
||||
milestones =
|
||||
[
|
||||
milestone1_project1,
|
||||
milestone1_project2,
|
||||
milestone1_project3,
|
||||
milestone2_project1,
|
||||
milestone2_project2,
|
||||
milestone2_project3
|
||||
]
|
||||
@group_milestones = Milestones::GroupService.new(milestones).execute
|
||||
end
|
||||
|
||||
it 'should have all project milestones' do
|
||||
expect(@group_milestones.count).to eq(2)
|
||||
end
|
||||
|
||||
it 'should have all project milestones titles' do
|
||||
expect(@group_milestones.map { |group_milestone| group_milestone.title }).to match_array(['Milestone v1.2', 'VD-123'])
|
||||
end
|
||||
|
||||
it 'should have all project milestones' do
|
||||
expect(@group_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'milestone' do
|
||||
context 'with valid title' do
|
||||
before do
|
||||
milestones =
|
||||
[
|
||||
milestone1_project1,
|
||||
milestone1_project2,
|
||||
milestone1_project3,
|
||||
milestone2_project1,
|
||||
milestone2_project2,
|
||||
milestone2_project3
|
||||
]
|
||||
@group_milestones = Milestones::GroupService.new(milestones).milestone('Milestone v1.2')
|
||||
end
|
||||
|
||||
it 'should have exactly one group milestone' do
|
||||
expect(@group_milestones.title).to eq('Milestone v1.2')
|
||||
end
|
||||
|
||||
it 'should have all project milestones with the same title' do
|
||||
expect(@group_milestones.milestones.count).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue