Merge branch 'global-milestones' into 'master'

Create milestones in the group

When you work with groups its quite often you want to create same milestone in multiple projects. This MR allows you to do so

For #3488 

Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>

See merge request !1797
This commit is contained in:
Dmitriy Zaporozhets 2015-11-16 20:41:27 +00:00
commit 0061143ccd
37 changed files with 403 additions and 263 deletions

View file

@ -43,6 +43,8 @@ v 8.2.0 (unreleased)
- Ability to add release notes (markdown text and attachments) to git tags (aka Releases)
- Relative links from a repositories README.md now link to the default branch
- Fix trailing whitespace issue in merge request/issue title
- Fix bug when milestone/label filter was empty for dashboard issues page
- Add ability to create milestone in group projects from single form
v 8.1.4
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)

View file

@ -28,6 +28,8 @@ class Dispatcher
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
when 'projects:compare:show'
new Diff()
when 'projects:issues:new','projects:issues:edit'

View file

@ -0,0 +1,19 @@
module GlobalMilestones
extend ActiveSupport::Concern
def milestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end
def milestone
milestones = Milestone.of_projects(@projects).where(title: params[:title])
if milestones.present?
@milestone = GlobalMilestone.new(params[:title], milestones)
else
render_404
end
end
end

View file

@ -1,34 +1,19 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
before_action :load_projects
include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index
project_milestones = case params[:state]
when 'all'; state
when 'closed'; state('closed')
else state('active')
end
@dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
@dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
end
def show
project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
@dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end
private
def load_projects
@projects = current_user.authorized_projects.sorted_by_activity.non_archived
end
def title
params[:title]
end
def state(state = nil)
conditions = { project_id: @projects }
conditions.reverse_merge!(state: state) if state
Milestone.where(conditions).order("title ASC")
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end

View file

@ -1,5 +1,6 @@
class DashboardController < Dashboard::ApplicationController
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
respond_to :html
@ -47,4 +48,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end

View file

@ -1,8 +1,13 @@
class Groups::ApplicationController < ApplicationController
layout 'group'
before_action :group
private
def group
@group ||= Group.find_by(path: params[:group_id])
end
def authorize_read_group!
unless @group and can?(current_user, :read_group, @group)
if current_user.nil?

View file

@ -1,8 +1,6 @@
class Groups::AvatarsController < ApplicationController
class Groups::AvatarsController < Groups::ApplicationController
def destroy
@group = Group.find_by(path: params[:group_id])
@group.remove_avatar!
@group.save
redirect_to edit_group_path(@group)

View file

@ -1,6 +1,5 @@
class Groups::GroupMembersController < Groups::ApplicationController
skip_before_action :authenticate_user!, only: [:index]
before_action :group
# Authorize
before_action :authorize_read_group!
@ -80,10 +79,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def group
@group ||= Group.find_by(path: params[:group_id])
end
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end

View file

@ -1,54 +1,55 @@
class Groups::MilestonesController < Groups::ApplicationController
before_action :authorize_group_milestone!, only: :update
include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show, :update]
before_action :authorize_group_milestone!, only: [:create, :update]
def index
project_milestones = case params[:state]
when 'all'; state
when 'closed'; state('closed')
else state('active')
end
@group_milestones = Milestones::GroupService.new(project_milestones).execute
@group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
end
def new
@milestone = Milestone.new
end
def create
project_ids = params[:milestone][:project_ids]
title = milestone_params[:title]
@group.projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
redirect_to milestone_path(title)
end
def show
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
@group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end
def update
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
@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)
@milestone.milestones.each do |milestone|
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
respond_to do |format|
format.js
format.html do
redirect_to group_milestones_path(group)
end
end
redirect_back_or_default(default: milestone_path(@milestone.title))
end
private
def group
@group ||= Group.find_by(path: params[:group_id])
end
def title
params[:title]
end
def state(state = nil)
conditions = { project_id: group.projects }
conditions.reverse_merge!(state: state) if state
Milestone.where(conditions).order("title ASC")
end
def authorize_group_milestone!
return render_404 unless can?(current_user, :admin_group, group)
return render_404 unless can?(current_user, :admin_milestones, group)
end
def milestone_params
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
end
def milestone_path(title)
group_milestone_path(@group, title.parameterize, title: title)
end
def projects
@projects ||= @group.projects
end
end

View file

@ -0,0 +1,12 @@
class MilestonesFinder
def execute(projects, params)
milestones = Milestone.of_projects(projects)
milestones = milestones.order("due_date ASC")
case params[:state]
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
end
end
end

View file

@ -100,7 +100,7 @@ module LabelsHelper
Label.where(project_id: @projects)
end
grouped_labels = Labels::GroupService.new(labels).execute
grouped_labels = GlobalLabel.build_collection(labels)
grouped_labels.unshift(Label::None)
grouped_labels.unshift(Label::Any)

View file

@ -28,7 +28,7 @@ module MilestonesHelper
Milestone.where(project_id: @projects)
end.active
grouped_milestones = Milestones::GroupService.new(milestones).execute
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)

View file

@ -233,6 +233,7 @@ class Ability
if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules.push(*[
:create_projects,
:admin_milestones
])
end

View file

@ -0,0 +1,17 @@
class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, label|
new(title, label)
end
end
def initialize(title, labels)
@title = title
@labels = labels
end
end

View file

@ -1,7 +1,15 @@
class GroupMilestone
class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
new(title, milestones)
end
end
def initialize(title, milestones)
@title = title
@milestones = milestones
@ -60,15 +68,15 @@ class GroupMilestone
end
def issues
@group_issues ||= milestones.map(&:issues).flatten.group_by(&:state)
@issues ||= milestones.map(&:issues).flatten.group_by(&:state)
end
def merge_requests
@group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
@merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
end
def participants
@group_participants ||= milestones.map(&:participants).flatten.compact.uniq
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
@ -86,4 +94,8 @@ class GroupMilestone
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
def complete?
total_items_count == closed_items_count
end
end

View file

@ -1,9 +0,0 @@
class GroupLabel
attr_accessor :title, :labels
alias_attribute :name, :title
def initialize(title, labels)
@title = title
@labels = labels
end
end

View file

@ -1,26 +0,0 @@
module Labels
class GroupService < ::BaseService
def initialize(project_labels)
@project_labels = project_labels.group_by(&:title)
end
def execute
build(@project_labels)
end
def label(title)
if title
group_label = @project_labels[title].group_by(&:title)
build(group_label).first
else
nil
end
end
private
def build(label)
label.map { |title, labels| GroupLabel.new(title, labels) }
end
end
end

View file

@ -1,26 +0,0 @@
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

View file

@ -10,10 +10,10 @@
.milestones
%ul.content-list
- if @dashboard_milestones.blank?
- if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- @dashboard_milestones.each do |milestone|
- @milestones.each do |milestone|
= render 'milestone', milestone: milestone
= paginate @dashboard_milestones, theme: "gitlab"
= paginate @milestones, theme: "gitlab"

View file

@ -1,14 +1,14 @@
- page_title @dashboard_milestone.title, "Milestones"
- page_title @milestone.title, "Milestones"
%h4.page-title
.issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" }
- if @dashboard_milestone.closed?
.issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @milestone.closed?
Closed
- else
Open
Milestone #{@dashboard_milestone.title}
Milestone #{@milestone.title}
%hr
- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active?
- if @milestone.complete? && @milestone.active?
.alert.alert-success
%span All issues for this milestone are closed. You may close the milestone now.
@ -22,7 +22,7 @@
%th Open issues
%th State
%th Due date
- @dashboard_milestone.milestones.each do |milestone|
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
@ -39,46 +39,46 @@
.context
%p.lead
Progress:
#{@dashboard_milestone.closed_items_count} closed
#{@milestone.closed_items_count} closed
&ndash;
#{@dashboard_milestone.open_items_count} open
= milestone_progress_bar(@dashboard_milestone)
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav.nav-tabs
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @dashboard_milestone.issue_count
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @dashboard_milestone.merge_requests_count
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @dashboard_milestone.participants.count
%span.badge= @milestone.participants.count
.pull-right
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped"
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
.tab-content
.tab-pane.active#tab-issues
.row
.col-md-6
= render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.row
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
%ul.bordered-list
- @dashboard_milestone.participants.each do |user|
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"

View file

@ -22,7 +22,7 @@
%span.label.label-gray
= milestone.project.name
.col-sm-6
- if can?(current_user, :admin_group, @group)
- if can?(current_user, :admin_milestones, @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-xs btn-grouped btn-reopen"
- else

View file

@ -3,15 +3,22 @@
= render 'shared/milestones_filter'
.gray-content-block
Only milestones from
%strong #{@group.name}
group are listed here.
- if can?(current_user, :admin_milestones, @group)
.pull-right
%span.pull-right.hidden-xs
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
New Milestone
.oneline
Only milestones from
%strong #{@group.name}
group are listed here.
.milestones
%ul.content-list
- if @group_milestones.blank?
- if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- @group_milestones.each do |milestone|
- @milestones.each do |milestone|
= render 'milestone', milestone: milestone
= paginate @group_milestones, theme: "gitlab"
= paginate @milestones, theme: "gitlab"

View file

@ -0,0 +1,48 @@
- page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
%h3.page-title
New Milestone
%p.light
This will create milestone in every selected project
%hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
.row
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true
%p.hint Required
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
.clearfix
.error-alert
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects, :id, :name,
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
.col-sm-10= f.hidden_field :due_date
.col-sm-10
.datepicker
.form-actions
= f.submit 'Create Milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
:javascript
$(".datepicker").datepicker({
dateFormat: "yy-mm-dd",
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));

View file

@ -1,22 +1,22 @@
- page_title @group_milestone.title, "Milestones"
- page_title @milestone.title, "Milestones"
= render "header_title"
%h4.page-title
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
- if @group_milestone.closed?
.issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @milestone.closed?
Closed
- else
Open
Milestone #{@group_milestone.title}
Milestone #{@milestone.title}
.pull-right
- if can?(current_user, :admin_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-sm btn-close"
- if can?(current_user, :admin_milestones, @group)
- if @milestone.active?
= link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- 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-sm btn-grouped btn-reopen"
= link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
%hr
- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
- if @milestone.complete? && @milestone.active?
.alert.alert-success
%span All issues for this milestone are closed. You may close the milestone now.
@ -30,7 +30,7 @@
%th Open issues
%th State
%th Due date
- @group_milestone.milestones.each do |milestone|
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
@ -47,46 +47,46 @@
.context
%p.lead
Progress:
#{@group_milestone.closed_items_count} closed
#{@milestone.closed_items_count} closed
&ndash;
#{@group_milestone.open_items_count} open
= milestone_progress_bar(@group_milestone)
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav.nav-tabs
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @group_milestone.issue_count
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @group_milestone.merge_requests_count
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @group_milestone.participants.count
%span.badge= @milestone.participants.count
.pull-right
= link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped"
= link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
.tab-content
.tab-pane.active#tab-issues
.row
.col-md-6
= render 'issues', title: "Open", issues: @group_milestone.opened_issues
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @group_milestone.closed_issues
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.row
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
%ul.bordered-list
- @group_milestone.participants.each do |user|
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"

View file

@ -23,9 +23,7 @@
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
.hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
.pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
= render 'projects/notes/hints'
.clearfix
.error-alert
.col-md-6
@ -45,7 +43,7 @@
:javascript
$( ".datepicker" ).datepicker({
$(".datepicker").datepicker({
dateFormat: "yy-mm-dd",
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));

View file

@ -368,7 +368,7 @@ Gitlab::Application.routes.draw do
end
resource :avatar, only: [:destroy]
resources :milestones, only: [:index, :show, :update]
resources :milestones, only: [:index, :show, :update, :new, :create]
end
end

View file

@ -14,5 +14,6 @@
- [Protected branches](protected_branches.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)

View file

@ -0,0 +1,13 @@
# Milestones
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
![milestone form](milestones/form.png)
## Groups and milestones
You can create a milestone for several projects in the same group simultaneously.
On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
![group milestone form](milestones/group_form.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -153,6 +153,13 @@ Feature: Groups
Then I should see group milestone with descriptions and expiry date
And I should see group milestone with all issues and MRs assigned to that milestone
Scenario: Create multiple milestones with one form
Given I visit group "Owned" milestones page
And I click new milestone button
And I fill milestone name
When I press create mileston button
Then milestone in each project should be created
# Group projects in settings
Scenario: I should see all projects in the project list in settings
Given Group "Owned" has archived project

View file

@ -255,6 +255,28 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
step 'I fill milestone name' do
fill_in 'milestone_title', with: 'v2.9.0'
end
step 'I click new milestone button' do
click_link "New Milestone"
end
step 'I press create mileston button' do
click_button "Create Milestone"
end
step 'milestone in each project should be created' do
group = Group.find_by(name: 'Owned')
expect(page).to have_content "Milestone v2.9.0"
expect(group.projects).to be_present
group.projects.each do |project|
expect(page).to have_content project.name
end
end
protected
def assigned_to_me(key)

View file

@ -31,6 +31,10 @@ module SharedPaths
visit merge_requests_group_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" milestones page' do
visit group_milestones_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" members page' do
visit group_group_members_path(Group.find_by(name: "Owned"))
end

View file

@ -0,0 +1,65 @@
require 'spec_helper'
describe GlobalMilestone 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 :build_collection do
before do
milestones =
[
milestone1_project1,
milestone1_project2,
milestone1_project3,
milestone2_project1,
milestone2_project2,
milestone2_project3
]
@global_milestones = GlobalMilestone.build_collection(milestones)
end
it 'should have all project milestones' do
expect(@global_milestones.count).to eq(2)
end
it 'should have all project milestones titles' do
expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
end
it 'should have all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
end
describe :initialize do
before do
milestones =
[
milestone1_project1,
milestone1_project2,
milestone1_project3,
]
@global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
end
it 'should have exactly one group milestone' do
expect(@global_milestone.title).to eq('Milestone v1.2')
end
it 'should have all project milestones with the same title' do
expect(@global_milestone.milestones.count).to eq(3)
end
end
end

View file

@ -0,0 +1,28 @@
require 'spec_helper'
describe Milestones::CloseService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
before do
project.team << [user, :master]
end
describe :execute do
before do
Milestones::CloseService.new(project, user, {}).execute(milestone)
end
it { expect(milestone).to be_valid }
it { expect(milestone).to be_closed }
describe :event do
let(:event) { Event.first }
it { expect(event.milestone).to be_truthy }
it { expect(event.target).to eq(milestone) }
it { expect(event.action_name).to eq('closed') }
end
end
end

View file

@ -0,0 +1,24 @@
require 'spec_helper'
describe Milestones::CreateService do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
describe :execute do
context "valid params" do
before do
project.team << [user, :master]
opts = {
title: 'v2.1.9',
description: 'Patch release to fix security issue'
}
@milestone = Milestones::CreateService.new(project, user, opts).execute
end
it { expect(@milestone).to be_valid }
it { expect(@milestone.title).to eq('v2.1.9') }
end
end
end

View file

@ -1,70 +0,0 @@
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