Commit 3789cfe0 authored by Valery Sizov's avatar Valery Sizov

Add a starting date to milestones

parent d7eeb6df
......@@ -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
&middot;
= @milestone.expires_at
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
......
- 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)
.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
&middot;
= milestone.expires_at
= milestone_date_range(milestone)
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
......
---
title: Add a starting date to milestones
merge_request:
author:
# 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
......
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
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
......
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