Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2014-07-01 11:18:15 +0400
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2014-07-01 11:18:15 +0400
commit8fb76a8308292d991a32ea76e50fb5ecfa3284b2 (patch)
treee874cb67947340fea027e94e7f5cae9e57d8d992
parentbcfcd243a635a1ce655440562897c3ef9603eda9 (diff)
parent3e52517da1c506885c06c13ff59ff5863bcb68ea (diff)
Merge branch 'group_milestones' into 'master'
Group milestones
-rw-r--r--app/assets/stylesheets/sections/groups.scss4
-rw-r--r--app/controllers/groups/milestones_controller.rb54
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/models/group_milestone.rb95
-rw-r--r--app/services/milestones/group_service.rb26
-rw-r--r--app/views/groups/_filter.html.haml12
-rw-r--r--app/views/groups/milestones/_issue.html.haml10
-rw-r--r--app/views/groups/milestones/_issues.html.haml6
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml10
-rw-r--r--app/views/groups/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/groups/milestones/index.html.haml49
-rw-r--r--app/views/groups/milestones/show.html.haml76
-rw-r--r--app/views/layouts/nav/_group.html.haml3
-rw-r--r--config/routes.rb2
-rw-r--r--features/group.feature19
-rw-r--r--features/steps/group/group.rb94
-rw-r--r--spec/services/milestones/group_service_spec.rb70
17 files changed, 547 insertions, 0 deletions
diff --git a/app/assets/stylesheets/sections/groups.scss b/app/assets/stylesheets/sections/groups.scss
index 60ec79acadb..e49fe1a9dd6 100644
--- a/app/assets/stylesheets/sections/groups.scss
+++ b/app/assets/stylesheets/sections/groups.scss
@@ -7,3 +7,7 @@
.member-search-form {
float: left;
}
+
+.milestone-row {
+ @include str-truncated(90%);
+}
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
new file mode 100644
index 00000000000..8dde8e91468
--- /dev/null
+++ b/app/controllers/groups/milestones_controller.rb
@@ -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
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index cfc9a572cac..0dc53dedeb7 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -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
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
new file mode 100644
index 00000000000..013f8db00a7
--- /dev/null
+++ b/app/models/group_milestone.rb
@@ -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
diff --git a/app/services/milestones/group_service.rb b/app/services/milestones/group_service.rb
new file mode 100644
index 00000000000..11d702f1e7b
--- /dev/null
+++ b/app/services/milestones/group_service.rb
@@ -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
diff --git a/app/views/groups/_filter.html.haml b/app/views/groups/_filter.html.haml
new file mode 100644
index 00000000000..393be3f1d12
--- /dev/null
+++ b/app/views/groups/_filter.html.haml
@@ -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
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
new file mode 100644
index 00000000000..c0cf56941f5
--- /dev/null
+++ b/app/views/groups/milestones/_issue.html.haml
@@ -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} &middot;
+ = 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"
diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml
new file mode 100644
index 00000000000..9f350b772bd
--- /dev/null
+++ b/app/views/groups/milestones/_issues.html.haml
@@ -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
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
new file mode 100644
index 00000000000..037162a20e0
--- /dev/null
+++ b/app/views/groups/milestones/_merge_request.html.haml
@@ -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} &middot;
+ = 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"
diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml
new file mode 100644
index 00000000000..50057e2c636
--- /dev/null
+++ b/app/views/groups/milestones/_merge_requests.html.haml
@@ -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
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
new file mode 100644
index 00000000000..9dd8fa96640
--- /dev/null
+++ b/app/views/groups/milestones/index.html.haml
@@ -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'
+ &nbsp;
+ = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
+ = pluralize milestone.merge_requests_count, 'Merge Request'
+ &nbsp;
+ %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
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
new file mode 100644
index 00000000000..d39870b032c
--- /dev/null
+++ b/app/views/groups/milestones/show.html.haml
@@ -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
+ &larr; 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
+ &ndash;
+ #{@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
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index d306e1eeb54..5d161a17bfc 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -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
diff --git a/config/routes.rb b/config/routes.rb
index 14ff52f387a..244cb339898 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/features/group.feature b/features/group.feature
index 71c28c07a3c..0c70e5b915c 100644
--- a/features/group.feature
+++ b/features/group.feature
@@ -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
diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb
index f321428592f..85276f04dd2 100644
--- a/features/steps/group/group.rb
+++ b/features/steps/group/group.rb
@@ -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
diff --git a/spec/services/milestones/group_service_spec.rb b/spec/services/milestones/group_service_spec.rb
new file mode 100644
index 00000000000..74eb0f99e0f
--- /dev/null
+++ b/spec/services/milestones/group_service_spec.rb
@@ -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