Commit 0061143c authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

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: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>

See merge request !1797
parents 66c76053 b093f509
......@@ -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)
......
......@@ -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'
......
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
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
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
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?
......
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)
......
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
......
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 show
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
@group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
def new
@milestone = Milestone.new
end
def update
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
@group_milestones = Milestones::GroupService.new(project_milestones).milestone(title)
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
@group_milestones.milestones.each do |milestone|
Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
redirect_to milestone_path(title)
end
respond_to do |format|
format.js
format.html do
redirect_to group_milestones_path(group)
def show
end
def update
@milestone.milestones.each do |milestone|
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
redirect_back_or_default(default: milestone_path(@milestone.title))
end
private
def group
@group ||= Group.find_by(path: params[:group_id])
def authorize_group_milestone!
return render_404 unless can?(current_user, :admin_milestones, group)
end
def title
params[:title]
def milestone_params
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
end
def state(state = nil)
conditions = { project_id: group.projects }
conditions.reverse_merge!(state: state) if state
Milestone.where(conditions).order("title ASC")
def milestone_path(title)
group_milestone_path(@group, title.parameterize, title: title)
end
def authorize_group_milestone!
return render_404 unless can?(current_user, :admin_group, group)
def projects
@projects ||= @group.projects
end
end
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
......@@ -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)
......
......@@ -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)
......
......@@ -233,6 +233,7 @@ class Ability
if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules.push(*[
:create_projects,
:admin_milestones
])
end
......
class GroupLabel
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
......
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
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
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
......@@ -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"
- 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"
......
......@@ -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
......
......@@ -3,15 +3,22 @@
= render 'shared/milestones_filter'
.gray-content-block
- 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"
- 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()));
- 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"
......
......@@ -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()));
......@@ -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
......
......@@ -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)
# 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)
......@@ -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
......
......@@ -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)
......
......@@ -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
......
require 'spec_helper'
describe Milestones::GroupService do
describe GlobalMilestone do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) }
......@@ -14,8 +14,7 @@ describe Milestones::GroupService do
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
describe :build_collection do
before do
milestones =
[
......@@ -26,45 +25,41 @@ describe Milestones::GroupService do
milestone2_project2,
milestone2_project3
]
@group_milestones = Milestones::GroupService.new(milestones).execute
@global_milestones = GlobalMilestone.build_collection(milestones)
end
it 'should have all project milestones' do
expect(@group_milestones.count).to eq(2)
expect(@global_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'])
expect(@global_milestones.map(&: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
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
end
describe 'milestone' do
context 'with valid title' do
describe :initialize 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')
@global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
end
it 'should have exactly one group milestone' do
expect(@group_milestones.title).to eq('Milestone v1.2')
expect(@global_milestone.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
expect(@global_milestone.milestones.count).to eq(3)
end
end
end
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
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment