Merge branch 'milestone_start_date' into 'master'
Add a starting date to milestones Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/23704 See merge request !7484
This commit is contained in:
commit
4646d453b3
|
@ -145,25 +145,19 @@
|
|||
|
||||
class DueDateSelectors {
|
||||
constructor() {
|
||||
this.initMilestoneDueDate();
|
||||
this.initMilestoneDatePicker();
|
||||
this.initIssuableSelect();
|
||||
}
|
||||
|
||||
initMilestoneDueDate() {
|
||||
const $datePicker = $('.datepicker');
|
||||
initMilestoneDatePicker() {
|
||||
$('.datepicker').datepicker({
|
||||
dateFormat: 'yy-mm-dd'
|
||||
});
|
||||
|
||||
if ($datePicker.length) {
|
||||
const $dueDate = $('#milestone_due_date');
|
||||
$datePicker.datepicker({
|
||||
dateFormat: 'yy-mm-dd',
|
||||
onSelect: (dateText, inst) => {
|
||||
$dueDate.val(dateText);
|
||||
}
|
||||
}).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
|
||||
}
|
||||
$('.js-clear-due-date').on('click', (e) => {
|
||||
$('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
$.datepicker._clearDate($datePicker);
|
||||
const datepicker = $(e.target).siblings('.datepicker');
|
||||
$.datepicker._clearDate(datepicker);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -39,4 +39,8 @@
|
|||
&.status-box-expired {
|
||||
background: #cea61b;
|
||||
}
|
||||
|
||||
&.status-box-upcoming {
|
||||
background: #8f8f8f;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def milestone_params
|
||||
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
|
||||
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
||||
end
|
||||
|
||||
def milestone_path(title)
|
||||
|
|
|
@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def milestone_params
|
||||
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
|
||||
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,6 +64,8 @@ module IssuesHelper
|
|||
'status-box-merged'
|
||||
elsif item.closed?
|
||||
'status-box-closed'
|
||||
elsif item.respond_to?(:upcoming?) && item.upcoming?
|
||||
'status-box-upcoming'
|
||||
else
|
||||
'status-box-open'
|
||||
end
|
||||
|
|
|
@ -86,6 +86,30 @@ module MilestonesHelper
|
|||
days = milestone.remaining_days
|
||||
content = content_tag(:strong, days)
|
||||
content << " #{'day'.pluralize(days)} remaining"
|
||||
elsif milestone.upcoming?
|
||||
content_tag(:strong, 'Upcoming')
|
||||
elsif milestone.start_date && milestone.start_date.past?
|
||||
days = milestone.elapsed_days
|
||||
content = content_tag(:strong, days)
|
||||
content << " #{'day'.pluralize(days)} elapsed"
|
||||
end
|
||||
end
|
||||
|
||||
def milestone_date_range(milestone)
|
||||
if milestone.start_date && milestone.due_date
|
||||
"#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
|
||||
elsif milestone.due_date
|
||||
if milestone.due_date.past?
|
||||
"expired on #{milestone.due_date.to_s(:medium)}"
|
||||
else
|
||||
"expires on #{milestone.due_date.to_s(:medium)}"
|
||||
end
|
||||
elsif milestone.start_date
|
||||
if milestone.start_date.past?
|
||||
"started on #{milestone.start_date.to_s(:medium)}"
|
||||
else
|
||||
"starts on #{milestone.start_date.to_s(:medium)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,31 @@ module Milestoneish
|
|||
(due_date - Date.today).to_i
|
||||
end
|
||||
|
||||
def elapsed_days
|
||||
return 0 if !start_date || start_date.future?
|
||||
|
||||
(Date.today - start_date).to_i
|
||||
end
|
||||
|
||||
def issues_visible_to_user(user = nil)
|
||||
issues.visible_to_user(user)
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
start_date && start_date.future?
|
||||
end
|
||||
|
||||
def expires_at
|
||||
if due_date
|
||||
if due_date.past?
|
||||
"expired on #{due_date.to_s(:medium)}"
|
||||
else
|
||||
"expires on #{due_date.to_s(:medium)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expired?
|
||||
due_date && due_date.past?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,26 +28,16 @@ class GlobalMilestone
|
|||
@title.to_slug.normalize.to_s
|
||||
end
|
||||
|
||||
def expired?
|
||||
if due_date
|
||||
due_date.past?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= Project.for_milestones(milestones.select(:id))
|
||||
end
|
||||
|
||||
def state
|
||||
state = milestones.map { |milestone| milestone.state }
|
||||
|
||||
if state.count('closed') == state.size
|
||||
'closed'
|
||||
else
|
||||
'active'
|
||||
milestones.each do |milestone|
|
||||
return 'active' if milestone.state != 'closed'
|
||||
end
|
||||
|
||||
'closed'
|
||||
end
|
||||
|
||||
def active?
|
||||
|
@ -81,18 +71,15 @@ class GlobalMilestone
|
|||
@due_date =
|
||||
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
|
||||
@milestones.first.due_date
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def expires_at
|
||||
if due_date
|
||||
if due_date.past?
|
||||
"expired on #{due_date.to_s(:medium)}"
|
||||
else
|
||||
"expires on #{due_date.to_s(:medium)}"
|
||||
def start_date
|
||||
return @start_date if defined?(@start_date)
|
||||
|
||||
@start_date =
|
||||
if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
|
||||
@milestones.first.start_date
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base
|
|||
|
||||
validates :title, presence: true, uniqueness: { scope: :project_id }
|
||||
validates :project, presence: true
|
||||
validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
|
||||
|
||||
strip_attributes :title
|
||||
|
||||
|
@ -131,24 +132,6 @@ class Milestone < ActiveRecord::Base
|
|||
self.title
|
||||
end
|
||||
|
||||
def expired?
|
||||
if due_date
|
||||
due_date.past?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def expires_at
|
||||
if due_date
|
||||
if due_date.past?
|
||||
"expired on #{due_date.to_s(:medium)}"
|
||||
else
|
||||
"expires on #{due_date.to_s(:medium)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_closed?
|
||||
active? && issues.opened.count.zero?
|
||||
end
|
||||
|
@ -212,4 +195,10 @@ class Milestone < ActiveRecord::Base
|
|||
def sanitize_title(value)
|
||||
CGI.unescape_html(Sanitize.clean(value.to_s))
|
||||
end
|
||||
|
||||
def start_date_should_be_less_than_due_date
|
||||
if due_date <= start_date
|
||||
errors.add(:start_date, "Can't be greater than due date")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,19 +36,8 @@
|
|||
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
|
||||
{ selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
|
||||
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :due_date, "Due Date", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
|
||||
= render "shared/milestones/form_dates", f: f
|
||||
|
||||
.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()));
|
||||
|
|
|
@ -14,12 +14,7 @@
|
|||
= render 'projects/notes/hints'
|
||||
.clearfix
|
||||
.error-alert
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :due_date, "Due Date", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
|
||||
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
|
||||
= render "shared/milestones/form_dates", f: f
|
||||
|
||||
.form-actions
|
||||
- if @milestone.new_record?
|
||||
|
|
|
@ -10,15 +10,17 @@
|
|||
Closed
|
||||
- elsif @milestone.expired?
|
||||
Past due
|
||||
- elsif @milestone.upcoming?
|
||||
Upcoming
|
||||
- else
|
||||
Open
|
||||
.header-text-content
|
||||
%span.identifier
|
||||
Milestone ##{@milestone.iid}
|
||||
- if @milestone.expires_at
|
||||
- if @milestone.due_date || @milestone.start_date
|
||||
%span.creator
|
||||
·
|
||||
= @milestone.expires_at
|
||||
= milestone_date_range(@milestone)
|
||||
.milestone-buttons
|
||||
- if can?(current_user, :admin_milestone, @project)
|
||||
- if @milestone.active?
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
- if milestone.expired? and not milestone.closed?
|
||||
%span.cred (Expired)
|
||||
- if milestone.expires_at
|
||||
- if milestone.upcoming?
|
||||
%span.clgray (Upcoming)
|
||||
- if milestone.due_date || milestone.start_date
|
||||
%span
|
||||
= milestone.expires_at
|
||||
= milestone_date_range(milestone)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.col-md-6
|
||||
.form-group
|
||||
= f.label :start_date, "Start Date", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
|
||||
%a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
|
||||
.col-md-6
|
||||
.form-group
|
||||
= f.label :due_date, "Due Date", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
|
||||
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
|
||||
|
||||
:javascript
|
||||
new gl.DueDateSelectors();
|
|
@ -12,10 +12,10 @@
|
|||
Open
|
||||
%span.identifier
|
||||
Milestone #{milestone.title}
|
||||
- if milestone.expires_at
|
||||
- if milestone.due_date || milestone.start_date
|
||||
%span.creator
|
||||
·
|
||||
= milestone.expires_at
|
||||
= milestone_date_range(milestone)
|
||||
- if group
|
||||
.pull-right
|
||||
- if can?(current_user, :admin_milestones, group)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add a starting date to milestones
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,12 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddStartDateToMilestones < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :milestones, :start_date, :date
|
||||
end
|
||||
end
|
|
@ -720,6 +720,7 @@ ActiveRecord::Schema.define(version: 20161118183841) do
|
|||
t.integer "iid"
|
||||
t.text "title_html"
|
||||
t.text "description_html"
|
||||
t.date "start_date"
|
||||
end
|
||||
|
||||
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
|
||||
|
|
|
@ -35,6 +35,7 @@ Example Response:
|
|||
"title": "10.0",
|
||||
"description": "Version",
|
||||
"due_date": "2013-11-29",
|
||||
"start_date": "2013-11-10",
|
||||
"state": "active",
|
||||
"updated_at": "2013-10-02T09:24:18Z",
|
||||
"created_at": "2013-10-02T09:24:18Z"
|
||||
|
@ -70,6 +71,7 @@ Parameters:
|
|||
- `title` (required) - The title of an milestone
|
||||
- `description` (optional) - The description of the milestone
|
||||
- `due_date` (optional) - The due date of the milestone
|
||||
- `start_date` (optional) - The start date of the milestone
|
||||
|
||||
## Edit milestone
|
||||
|
||||
|
@ -86,6 +88,7 @@ Parameters:
|
|||
- `title` (optional) - The title of a milestone
|
||||
- `description` (optional) - The description of a milestone
|
||||
- `due_date` (optional) - The due date of the milestone
|
||||
- `start_date` (optional) - The start date of the milestone
|
||||
- `state_event` (optional) - The state event of the milestone (close|activate)
|
||||
|
||||
## Get all issues assigned to a single milestone
|
||||
|
|
|
@ -210,6 +210,7 @@ module API
|
|||
|
||||
class Milestone < ProjectEntity
|
||||
expose :due_date
|
||||
expose :start_date
|
||||
end
|
||||
|
||||
class Issue < ProjectEntity
|
||||
|
|
|
@ -14,7 +14,8 @@ module API
|
|||
|
||||
params :optional_params do
|
||||
optional :description, type: String, desc: 'The description of the milestone'
|
||||
optional :due_date, type: String, desc: 'The due date of the milestone'
|
||||
optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
|
||||
optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,12 +14,17 @@ feature 'Milestone', feature: true do
|
|||
feature 'Create a milestone' do
|
||||
scenario 'shows an informative message for a new milestone' do
|
||||
visit new_namespace_project_milestone_path(project.namespace, project)
|
||||
|
||||
page.within '.milestone-form' do
|
||||
fill_in "milestone_title", with: '8.7'
|
||||
fill_in "milestone_start_date", with: '2016-11-16'
|
||||
fill_in "milestone_due_date", with: '2016-12-16'
|
||||
end
|
||||
|
||||
find('input[name="commit"]').click
|
||||
|
||||
expect(find('.alert-success')).to have_content('Assign some issues to this milestone.')
|
||||
expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe MilestonesHelper do
|
||||
describe "#milestone_date_range" do
|
||||
def result_for(*args)
|
||||
milestone_date_range(build(:milestone, *args))
|
||||
end
|
||||
|
||||
let(:yesterday) { Date.yesterday }
|
||||
let(:tomorrow) { yesterday + 2 }
|
||||
let(:format) { '%b %-d, %Y' }
|
||||
let(:yesterday_formatted) { yesterday.strftime(format) }
|
||||
let(:tomorrow_formatted) { tomorrow.strftime(format) }
|
||||
|
||||
it { expect(result_for(due_date: nil, start_date: nil)).to be_nil }
|
||||
it { expect(result_for(due_date: tomorrow)).to eq("expires on #{tomorrow_formatted}") }
|
||||
it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") }
|
||||
it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") }
|
||||
it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") }
|
||||
it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") }
|
||||
end
|
||||
|
||||
describe '#milestone_counts' do
|
||||
let(:project) { FactoryGirl.create(:project) }
|
||||
let(:counts) { helper.milestone_counts(project.milestones) }
|
||||
|
|
|
@ -78,6 +78,7 @@ Milestone:
|
|||
- project_id
|
||||
- description
|
||||
- due_date
|
||||
- start_date
|
||||
- created_at
|
||||
- updated_at
|
||||
- state
|
||||
|
|
|
@ -115,4 +115,24 @@ describe Milestone, 'Milestoneish' do
|
|||
expect(milestone.percent_complete(admin)).to eq 60
|
||||
end
|
||||
end
|
||||
|
||||
describe '#elapsed_days' do
|
||||
it 'shows 0 if no start_date set' do
|
||||
milestone = build(:milestone)
|
||||
|
||||
expect(milestone.elapsed_days).to eq(0)
|
||||
end
|
||||
|
||||
it 'shows 0 if start_date is a future' do
|
||||
milestone = build(:milestone, start_date: Time.now + 2.days)
|
||||
|
||||
expect(milestone.elapsed_days).to eq(0)
|
||||
end
|
||||
|
||||
it 'shows correct amount of days' do
|
||||
milestone = build(:milestone, start_date: Time.now - 2.days)
|
||||
|
||||
expect(milestone.elapsed_days).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Milestone, models: true do
|
||||
describe "Associations" do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to have_many(:issues) }
|
||||
end
|
||||
|
||||
describe "Validation" do
|
||||
before do
|
||||
allow(subject).to receive(:set_iid).and_return(false)
|
||||
|
@ -13,6 +8,20 @@ describe Milestone, models: true do
|
|||
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
|
||||
describe 'start_date' do
|
||||
it 'adds an error when start_date is greated then due_date' do
|
||||
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
|
||||
|
||||
expect(milestone).not_to be_valid
|
||||
expect(milestone.errors[:start_date]).to include("Can't be greater than due date")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Associations" do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to have_many(:issues) }
|
||||
end
|
||||
|
||||
let(:milestone) { create(:milestone) }
|
||||
|
@ -58,18 +67,6 @@ describe Milestone, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#expires_at" do
|
||||
it "is nil when due_date is unset" do
|
||||
milestone.update_attributes(due_date: nil)
|
||||
expect(milestone.expires_at).to be_nil
|
||||
end
|
||||
|
||||
it "is not nil when due_date is set" do
|
||||
milestone.update_attributes(due_date: Date.tomorrow)
|
||||
expect(milestone.expires_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe '#expired?' do
|
||||
context "expired" do
|
||||
before do
|
||||
|
@ -88,6 +85,18 @@ describe Milestone, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#upcoming?' do
|
||||
it 'returns true' do
|
||||
milestone = build(:milestone, start_date: Time.now + 1.month)
|
||||
expect(milestone.upcoming?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
milestone = build(:milestone, start_date: Date.today.prev_year)
|
||||
expect(milestone.upcoming?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#percent_complete' do
|
||||
before do
|
||||
allow(milestone).to receive_messages(
|
||||
|
|
|
@ -92,13 +92,14 @@ describe API::API, api: true do
|
|||
expect(json_response['description']).to be_nil
|
||||
end
|
||||
|
||||
it 'creates a new project milestone with description and due date' do
|
||||
it 'creates a new project milestone with description and dates' do
|
||||
post api("/projects/#{project.id}/milestones", user),
|
||||
title: 'new milestone', description: 'release', due_date: '2013-03-02'
|
||||
title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
|
||||
|
||||
expect(response).to have_http_status(201)
|
||||
expect(json_response['description']).to eq('release')
|
||||
expect(json_response['due_date']).to eq('2013-03-02')
|
||||
expect(json_response['start_date']).to eq('2013-02-02')
|
||||
end
|
||||
|
||||
it 'returns a 400 error if title is missing' do
|
||||
|
|
Loading…
Reference in New Issue