first commit
This commit is contained in:
Коммит
5bad8deaa0
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
config.js
|
|
@ -0,0 +1,34 @@
|
|||
# Github issues Gantt
|
||||
|
||||
|
||||
![Gantt Diagram](/neyric/gh-issues-gantt/raw/master/screenshot.png "GitHub Issues Gantt")
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Requires Node.js
|
||||
|
||||
|
||||
* Clone the git repository
|
||||
* From within the repository, type npm install
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
* edit config.js
|
||||
* edit public/config.js
|
||||
|
||||
|
||||
## In GitHub
|
||||
|
||||
* You can set the duration of each ticket by adding a label "1D" (1 day), "2D" (2 days), ...
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
* fix bug: when tickets overlap holidays
|
||||
|
||||
* Add a refresh button
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
module.exports = {
|
||||
username: 'gh-username',
|
||||
password: 'gh-password',
|
||||
repo: 'user/repo'
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "gh-issues-gantt",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"express": "3.0.6",
|
||||
"request": "2.12.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
var config = {
|
||||
|
||||
repo: 'user/repo',
|
||||
|
||||
weekDaysOff: [0,6],
|
||||
|
||||
colorByDev: {
|
||||
"neyric": "ganttBlue",
|
||||
"unassigned": "ganttRed"
|
||||
},
|
||||
|
||||
holidays: {
|
||||
"neyric": [
|
||||
{ start: new Date(2013, 0, 11), end: new Date(2013, 0, 11), title: 'Déménagement'}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
excludedMilestones: [
|
||||
"Feature Paradize"
|
||||
],
|
||||
|
||||
defaultDuration: 1 // in days
|
||||
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Add 'amount' days to this date and return a new date object
|
||||
*/
|
||||
Date.prototype.AddDays = function(amount) {
|
||||
var d = new Date(this.getTime());
|
||||
d.setDate(this.getDate() + amount);
|
||||
return d;
|
||||
};
|
||||
|
||||
|
||||
Date.prototype.weekNumber = function() {
|
||||
var MaDate = this;
|
||||
var mm = MaDate.getMonth(), jj = MaDate.getDate();
|
||||
|
||||
var annee = MaDate.getFullYear();
|
||||
var NumSemaine = 0,
|
||||
ListeMois = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
|
||||
|
||||
if (annee %4 == 0 && annee %100 !=0 || annee %400 == 0) {ListeMois[1]=29};
|
||||
var TotalJour=0;
|
||||
for(cpt=0; cpt<mm; cpt++){TotalJour+=ListeMois[cpt];}
|
||||
TotalJour+=jj;
|
||||
DebutAn = new Date(annee,0,1);
|
||||
var JourDebutAn;
|
||||
JourDebutAn=DebutAn.getDay();
|
||||
if(JourDebutAn==0){JourDebutAn=7};
|
||||
|
||||
TotalJour-=8-JourDebutAn;
|
||||
NumSemaine = 1;
|
||||
NumSemaine+=Math.floor(TotalJour/7);
|
||||
if(TotalJour%7!=0){NumSemaine+=1};
|
||||
|
||||
return(NumSemaine);
|
||||
}
|
||||
|
||||
Date.getToday = function() {
|
||||
var now = new Date();
|
||||
return now.getMidnight();
|
||||
};
|
||||
|
||||
Date.prototype.getMidnight = function() {
|
||||
return new Date( this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Github issues Gantt</title>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="http://taitems.github.com/jQuery.Gantt/css/style.css">
|
||||
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css">
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
padding: 0 0 50px 0;
|
||||
}
|
||||
.contain { width: 1400px;margin: 0 auto;}
|
||||
|
||||
.fn-gantt .leftPanel .fn-label, .fn-gantt .leftPanel .name { width: 190px; }
|
||||
.fn-gantt .leftPanel .desc { width: 195px; }
|
||||
.fn-gantt .leftPanel { width: 385px; }
|
||||
|
||||
.fn-gantt .ganttYellow {
|
||||
background-color: #FEEA77;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="contain">
|
||||
<h1>GitHub issues Gantt</h1>
|
||||
|
||||
<div id='msg' class="alert alert-block"></div>
|
||||
|
||||
<h2>By Milestone</h2>
|
||||
<div class="gantt-milestone"></div>
|
||||
|
||||
<h2>By Dev</h2>
|
||||
<div class="gantt-dev"></div>
|
||||
</div>
|
||||
|
||||
<script src="/lib/jquery-1.7.2.min.js"></script>
|
||||
<script src="/lib/jquery.fn.gantt.js"></script>
|
||||
<script src="/lib/bootstrap-tooltip.js"></script>
|
||||
<script src="/lib/bootstrap-popover.js"></script>
|
||||
|
||||
<!-- From server -->
|
||||
<script src="/issues.js"></script>
|
||||
<script src="/milestones.js"></script>
|
||||
|
||||
<script src="/config.js"></script>
|
||||
<script src="/date-functions.js"></script>
|
||||
<script src="/planning.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
"use strict";
|
||||
Planning.init();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,114 @@
|
|||
/* ===========================================================
|
||||
* bootstrap-popover.js v2.2.2
|
||||
* http://twitter.github.com/bootstrap/javascript.html#popovers
|
||||
* ===========================================================
|
||||
* Copyright 2012 Twitter, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* =========================================================== */
|
||||
|
||||
|
||||
!function ($) {
|
||||
|
||||
"use strict"; // jshint ;_;
|
||||
|
||||
|
||||
/* POPOVER PUBLIC CLASS DEFINITION
|
||||
* =============================== */
|
||||
|
||||
var Popover = function (element, options) {
|
||||
this.init('popover', element, options)
|
||||
}
|
||||
|
||||
|
||||
/* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js
|
||||
========================================== */
|
||||
|
||||
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, {
|
||||
|
||||
constructor: Popover
|
||||
|
||||
, setContent: function () {
|
||||
var $tip = this.tip()
|
||||
, title = this.getTitle()
|
||||
, content = this.getContent()
|
||||
|
||||
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
|
||||
$tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
|
||||
|
||||
$tip.removeClass('fade top bottom left right in')
|
||||
}
|
||||
|
||||
, hasContent: function () {
|
||||
return this.getTitle() || this.getContent()
|
||||
}
|
||||
|
||||
, getContent: function () {
|
||||
var content
|
||||
, $e = this.$element
|
||||
, o = this.options
|
||||
|
||||
content = $e.attr('data-content')
|
||||
|| (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
, tip: function () {
|
||||
if (!this.$tip) {
|
||||
this.$tip = $(this.options.template)
|
||||
}
|
||||
return this.$tip
|
||||
}
|
||||
|
||||
, destroy: function () {
|
||||
this.hide().$element.off('.' + this.type).removeData(this.type)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
/* POPOVER PLUGIN DEFINITION
|
||||
* ======================= */
|
||||
|
||||
var old = $.fn.popover
|
||||
|
||||
$.fn.popover = function (option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
, data = $this.data('popover')
|
||||
, options = typeof option == 'object' && option
|
||||
if (!data) $this.data('popover', (data = new Popover(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.popover.Constructor = Popover
|
||||
|
||||
$.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
|
||||
placement: 'right'
|
||||
, trigger: 'click'
|
||||
, content: ''
|
||||
, template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"></div></div></div>'
|
||||
})
|
||||
|
||||
|
||||
/* POPOVER NO CONFLICT
|
||||
* =================== */
|
||||
|
||||
$.fn.popover.noConflict = function () {
|
||||
$.fn.popover = old
|
||||
return this
|
||||
}
|
||||
|
||||
}(window.jQuery);
|
|
@ -0,0 +1,287 @@
|
|||
/* ===========================================================
|
||||
* bootstrap-tooltip.js v2.2.2
|
||||
* http://twitter.github.com/bootstrap/javascript.html#tooltips
|
||||
* Inspired by the original jQuery.tipsy by Jason Frame
|
||||
* ===========================================================
|
||||
* Copyright 2012 Twitter, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================== */
|
||||
|
||||
|
||||
!function ($) {
|
||||
|
||||
"use strict"; // jshint ;_;
|
||||
|
||||
|
||||
/* TOOLTIP PUBLIC CLASS DEFINITION
|
||||
* =============================== */
|
||||
|
||||
var Tooltip = function (element, options) {
|
||||
this.init('tooltip', element, options)
|
||||
}
|
||||
|
||||
Tooltip.prototype = {
|
||||
|
||||
constructor: Tooltip
|
||||
|
||||
, init: function (type, element, options) {
|
||||
var eventIn
|
||||
, eventOut
|
||||
|
||||
this.type = type
|
||||
this.$element = $(element)
|
||||
this.options = this.getOptions(options)
|
||||
this.enabled = true
|
||||
|
||||
if (this.options.trigger == 'click') {
|
||||
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
|
||||
} else if (this.options.trigger != 'manual') {
|
||||
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
|
||||
eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
|
||||
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
|
||||
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
|
||||
}
|
||||
|
||||
this.options.selector ?
|
||||
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
|
||||
this.fixTitle()
|
||||
}
|
||||
|
||||
, getOptions: function (options) {
|
||||
options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
|
||||
|
||||
if (options.delay && typeof options.delay == 'number') {
|
||||
options.delay = {
|
||||
show: options.delay
|
||||
, hide: options.delay
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
, enter: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
|
||||
if (!self.options.delay || !self.options.delay.show) return self.show()
|
||||
|
||||
clearTimeout(this.timeout)
|
||||
self.hoverState = 'in'
|
||||
this.timeout = setTimeout(function() {
|
||||
if (self.hoverState == 'in') self.show()
|
||||
}, self.options.delay.show)
|
||||
}
|
||||
|
||||
, leave: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout)
|
||||
if (!self.options.delay || !self.options.delay.hide) return self.hide()
|
||||
|
||||
self.hoverState = 'out'
|
||||
this.timeout = setTimeout(function() {
|
||||
if (self.hoverState == 'out') self.hide()
|
||||
}, self.options.delay.hide)
|
||||
}
|
||||
|
||||
, show: function () {
|
||||
var $tip
|
||||
, inside
|
||||
, pos
|
||||
, actualWidth
|
||||
, actualHeight
|
||||
, placement
|
||||
, tp
|
||||
|
||||
if (this.hasContent() && this.enabled) {
|
||||
$tip = this.tip()
|
||||
this.setContent()
|
||||
|
||||
if (this.options.animation) {
|
||||
$tip.addClass('fade')
|
||||
}
|
||||
|
||||
placement = typeof this.options.placement == 'function' ?
|
||||
this.options.placement.call(this, $tip[0], this.$element[0]) :
|
||||
this.options.placement
|
||||
|
||||
inside = /in/.test(placement)
|
||||
|
||||
$tip
|
||||
.detach()
|
||||
.css({ top: 0, left: 0, display: 'block' })
|
||||
.insertAfter(this.$element)
|
||||
|
||||
pos = this.getPosition(inside)
|
||||
|
||||
actualWidth = $tip[0].offsetWidth
|
||||
actualHeight = $tip[0].offsetHeight
|
||||
|
||||
switch (inside ? placement.split(' ')[1] : placement) {
|
||||
case 'bottom':
|
||||
tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
|
||||
break
|
||||
case 'top':
|
||||
tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
|
||||
break
|
||||
case 'left':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
|
||||
break
|
||||
case 'right':
|
||||
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
|
||||
break
|
||||
}
|
||||
|
||||
$tip
|
||||
.offset(tp)
|
||||
.addClass(placement)
|
||||
.addClass('in')
|
||||
}
|
||||
}
|
||||
|
||||
, setContent: function () {
|
||||
var $tip = this.tip()
|
||||
, title = this.getTitle()
|
||||
|
||||
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
|
||||
$tip.removeClass('fade in top bottom left right')
|
||||
}
|
||||
|
||||
, hide: function () {
|
||||
var that = this
|
||||
, $tip = this.tip()
|
||||
|
||||
$tip.removeClass('in')
|
||||
|
||||
function removeWithAnimation() {
|
||||
var timeout = setTimeout(function () {
|
||||
$tip.off($.support.transition.end).detach()
|
||||
}, 500)
|
||||
|
||||
$tip.one($.support.transition.end, function () {
|
||||
clearTimeout(timeout)
|
||||
$tip.detach()
|
||||
})
|
||||
}
|
||||
|
||||
$.support.transition && this.$tip.hasClass('fade') ?
|
||||
removeWithAnimation() :
|
||||
$tip.detach()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
, fixTitle: function () {
|
||||
var $e = this.$element
|
||||
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
|
||||
$e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
|
||||
}
|
||||
}
|
||||
|
||||
, hasContent: function () {
|
||||
return this.getTitle()
|
||||
}
|
||||
|
||||
, getPosition: function (inside) {
|
||||
return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
|
||||
width: this.$element[0].offsetWidth
|
||||
, height: this.$element[0].offsetHeight
|
||||
})
|
||||
}
|
||||
|
||||
, getTitle: function () {
|
||||
var title
|
||||
, $e = this.$element
|
||||
, o = this.options
|
||||
|
||||
title = $e.attr('data-original-title')
|
||||
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
, tip: function () {
|
||||
return this.$tip = this.$tip || $(this.options.template)
|
||||
}
|
||||
|
||||
, validate: function () {
|
||||
if (!this.$element[0].parentNode) {
|
||||
this.hide()
|
||||
this.$element = null
|
||||
this.options = null
|
||||
}
|
||||
}
|
||||
|
||||
, enable: function () {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
, disable: function () {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
, toggleEnabled: function () {
|
||||
this.enabled = !this.enabled
|
||||
}
|
||||
|
||||
, toggle: function (e) {
|
||||
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
|
||||
self[self.tip().hasClass('in') ? 'hide' : 'show']()
|
||||
}
|
||||
|
||||
, destroy: function () {
|
||||
this.hide().$element.off('.' + this.type).removeData(this.type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* TOOLTIP PLUGIN DEFINITION
|
||||
* ========================= */
|
||||
|
||||
var old = $.fn.tooltip
|
||||
|
||||
$.fn.tooltip = function ( option ) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
, data = $this.data('tooltip')
|
||||
, options = typeof option == 'object' && option
|
||||
if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.tooltip.Constructor = Tooltip
|
||||
|
||||
$.fn.tooltip.defaults = {
|
||||
animation: true
|
||||
, placement: 'top'
|
||||
, selector: false
|
||||
, template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
|
||||
, trigger: 'hover'
|
||||
, title: ''
|
||||
, delay: 0
|
||||
, html: false
|
||||
}
|
||||
|
||||
|
||||
/* TOOLTIP NO CONFLICT
|
||||
* =================== */
|
||||
|
||||
$.fn.tooltip.noConflict = function () {
|
||||
$.fn.tooltip = old
|
||||
return this
|
||||
}
|
||||
|
||||
}(window.jQuery);
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,319 @@
|
|||
/*global $,milestones,issues,window,config*/
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - Bug when tickets overlap holidays
|
||||
* - Doc for duration labels
|
||||
*/
|
||||
|
||||
var Planning = {
|
||||
|
||||
milestonesById: {},
|
||||
|
||||
developpers: {},
|
||||
|
||||
init: function() {
|
||||
|
||||
// Sort milestones by due date
|
||||
this.sorted_milestones = milestones.sort( function (a,b){
|
||||
return a.due_on > b.due_on ? 1 : -1;
|
||||
});
|
||||
|
||||
// Index milestones by id & add milestone issues
|
||||
milestones.forEach(function(milestone) {
|
||||
milestone.issues = [];
|
||||
this.milestonesById[milestone.id] = milestone;
|
||||
}, this);
|
||||
|
||||
// Run the plannification algorithm
|
||||
this.planner();
|
||||
|
||||
// Render the Gantt charts
|
||||
$(".gantt-milestone").gantt({
|
||||
source: this.getPlanningByMilestone(),
|
||||
navigate: "scroll",
|
||||
scale: "days",
|
||||
maxScale: "months",
|
||||
minScale: "days",
|
||||
itemsPerPage: 100,
|
||||
onItemClick: function(data) {
|
||||
Planning.onItemClick(data);
|
||||
}
|
||||
});
|
||||
|
||||
$(".gantt-dev").gantt({
|
||||
source: this.getPlanningForDevs(),
|
||||
navigate: "scroll",
|
||||
scale: "days",
|
||||
maxScale: "months",
|
||||
minScale: "days",
|
||||
itemsPerPage: 100,
|
||||
onItemClick: function(data) {
|
||||
Planning.onItemClick(data);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
onItemClick: function(data) {
|
||||
var baseUrl = 'https://github.com/'+config.repo, url;
|
||||
|
||||
if(data.issue) { url = baseUrl + '/issues/'+data.issue; }
|
||||
if(data.milestone) { url = baseUrl + '/issues?milestone='+data.milestone; }
|
||||
if(data.milestone_release) { url = baseUrl + '/issues/milestones/'+data.milestone_release+'/edit'; }
|
||||
|
||||
if(url) { window.open(url,"_blank"); }
|
||||
},
|
||||
|
||||
planner: function () {
|
||||
|
||||
// Dispatch issues in milestones
|
||||
issues.forEach(function(issue){
|
||||
if(!issue.milestone) {
|
||||
$('#msg').append("<p><a href='https://github.com/"+config.repo+"/issues/"+issue.number+"' target='new'>issue #"+issue.number+"</a> has no milestone !</p>");
|
||||
return;
|
||||
}
|
||||
this.milestonesById[issue.milestone.id].issues.push(issue);
|
||||
}, this);
|
||||
|
||||
|
||||
this.sorted_milestones.forEach(function(milestone) {
|
||||
|
||||
// assign tickets to each developper
|
||||
milestone.issues.forEach(function(issue) {
|
||||
|
||||
// Developpeur et sa date de dispo
|
||||
var dev = this.devNameFromIssue(issue);
|
||||
if(!this.developpers[dev]) {
|
||||
this.developpers[dev] = {
|
||||
next: Date.getToday(),
|
||||
issues: []
|
||||
};
|
||||
}
|
||||
|
||||
// Planification
|
||||
this.planIssueForDev(dev, issue);
|
||||
}, this);
|
||||
|
||||
}, this);
|
||||
|
||||
},
|
||||
|
||||
devNameFromIssue: function(issue) {
|
||||
return issue.assignee ? issue.assignee.login : 'unassigned';
|
||||
},
|
||||
|
||||
getDurationForIssue: function (issue) {
|
||||
|
||||
var numericLabels = issue.labels.filter(function(l){ return l.name.match(/^\d+D$/); }),
|
||||
name;
|
||||
|
||||
if(numericLabels.length > 0) {
|
||||
name = numericLabels[0].name;
|
||||
return parseInt( name.substr(0, name.length-1), 10);
|
||||
}
|
||||
|
||||
return config.defaultDuration;
|
||||
},
|
||||
|
||||
|
||||
planIssueForDev: function (dev, issue) {
|
||||
|
||||
// Planification
|
||||
var durationInDays = this.getDurationForIssue(issue);
|
||||
|
||||
var start = this.developpers[dev].next;
|
||||
|
||||
|
||||
// handle holidays
|
||||
var devHolidays = config.holidays[dev];
|
||||
if(devHolidays) {
|
||||
|
||||
devHolidays.forEach(function(holiday){
|
||||
if(start >= holiday.start && start <= holiday.end) {
|
||||
start = holiday.end.AddDays(1);
|
||||
}
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Skip start on week-end
|
||||
while( config.weekDaysOff.indexOf(start.getDay()) != -1 ) {
|
||||
start = start.AddDays(1);
|
||||
}
|
||||
|
||||
var end = start.AddDays(durationInDays);
|
||||
|
||||
// if [start,end] overlap week-ends, count the numbers of days to add
|
||||
if ( start.weekNumber() != end.weekNumber() ) {
|
||||
var diffDays = (end.weekNumber()-start.weekNumber())*2;
|
||||
end = end.AddDays(diffDays);
|
||||
}
|
||||
|
||||
|
||||
issue.planning = {
|
||||
start: start,
|
||||
end: end.AddDays(-1)
|
||||
};
|
||||
|
||||
this.developpers[dev].next = end;
|
||||
|
||||
this.developpers[dev].issues.push(issue);
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
genItemFromIssue: function (issue) {
|
||||
|
||||
var dev = this.devNameFromIssue(issue);
|
||||
|
||||
return {
|
||||
from: "/Date("+issue.planning.start.getTime()+")/",
|
||||
to: "/Date("+issue.planning.end.getTime()+")/",
|
||||
label: issue.title,
|
||||
desc: '#'+issue.number + ': ' + issue.title,
|
||||
customClass: config.colorByDev[dev] || "ganttRed",
|
||||
dataObj: {
|
||||
issue: issue.number
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Generate the planning for devs :
|
||||
*/
|
||||
getPlanningForDevs: function () {
|
||||
|
||||
var planning = [];
|
||||
|
||||
for(var dev in this.developpers) {
|
||||
|
||||
// Regroupe les tickets par milestone
|
||||
var hisMilestones = {};
|
||||
for(var i = 0 ; i < this.developpers[dev].issues.length ; i++) {
|
||||
var issue = this.developpers[dev].issues[i];
|
||||
|
||||
if(!hisMilestones[issue.milestone.title]) {
|
||||
hisMilestones[issue.milestone.title] = [];
|
||||
}
|
||||
|
||||
hisMilestones[issue.milestone.title].push( this.genItemFromIssue(issue));
|
||||
}
|
||||
|
||||
// Génère le planning
|
||||
var mIndex = 0;
|
||||
for(var m in hisMilestones) {
|
||||
|
||||
if(config.excludedMilestones.indexOf(m) != -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
planning.push({
|
||||
name: mIndex === 0 ? dev : " ",
|
||||
desc: m,
|
||||
values: hisMilestones[m]
|
||||
});
|
||||
mIndex++;
|
||||
}
|
||||
|
||||
if(config.holidays[dev]) {
|
||||
var values = config.holidays[dev].map(function(holiday) {
|
||||
return {
|
||||
from: "/Date("+holiday.start.getTime()+")/",
|
||||
to: "/Date("+holiday.end.getTime()+")/",
|
||||
label: holiday.title || 'holiday',
|
||||
desc: holiday.title || 'holiday',
|
||||
customClass: config.colorByDev[dev] || "ganttRed"
|
||||
};
|
||||
}, this);
|
||||
|
||||
planning.push({
|
||||
name: " ",
|
||||
desc: "holidays",
|
||||
values: values
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return planning;
|
||||
},
|
||||
|
||||
|
||||
getPlanningByMilestone: function () {
|
||||
|
||||
// for each milestone get issues assigned dates and group by assignee
|
||||
var planning = [];
|
||||
|
||||
for(var m = 0 ; m < this.sorted_milestones.length ; m++) {
|
||||
var milestone = this.sorted_milestones[m];
|
||||
|
||||
if(config.excludedMilestones.indexOf(milestone.title) != -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
var itsDevs = {};
|
||||
var min = (new Date(2018, 0, 1)).getTime(), max = (new Date()).getTime();
|
||||
|
||||
milestone.issues.forEach(function(issue){
|
||||
var dev = issue.assignee ? issue.assignee.login : 'unassigned';
|
||||
if(!itsDevs[dev]) { itsDevs[dev] = []; }
|
||||
itsDevs[dev].push( this.genItemFromIssue(issue));
|
||||
|
||||
if(issue.planning.start.getTime() < min)
|
||||
min = issue.planning.start.getTime();
|
||||
|
||||
if(issue.planning.end.getTime() > max)
|
||||
max = issue.planning.end.getTime();
|
||||
|
||||
}, this);
|
||||
|
||||
var releaseDate = (new Date(milestone.due_on)).getMidnight().getTime();
|
||||
|
||||
planning.push({
|
||||
name: milestone.title,
|
||||
desc: " ",
|
||||
values: [{
|
||||
from: "/Date("+min+")/",
|
||||
to: "/Date("+max+")/",
|
||||
label: milestone.title,
|
||||
desc: milestone.title,
|
||||
customClass: "ganttOrange",
|
||||
dataObj: {
|
||||
milestone: milestone.number
|
||||
}
|
||||
},
|
||||
{
|
||||
from: "/Date("+releaseDate+")/",
|
||||
to: "/Date("+releaseDate+")/",
|
||||
label: "★",
|
||||
desc: "Due date for : "+milestone.title,
|
||||
customClass: "ganttYellow",
|
||||
dataObj: {
|
||||
milestone_release: milestone.number
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Génère le planning
|
||||
for(var d in itsDevs) {
|
||||
planning.push({
|
||||
name: " ",
|
||||
desc: d,
|
||||
values: itsDevs[d]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return planning;
|
||||
}
|
||||
|
||||
};
|
||||
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 173 KiB |
|
@ -0,0 +1,84 @@
|
|||
var express = require('express'),
|
||||
request = require('request');
|
||||
|
||||
var app = express(),
|
||||
config = require('./config.js'),
|
||||
baseUrl = "https://api.github.com/repos/"+config.repo;
|
||||
|
||||
app.use(express["static"](__dirname + '/public'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
res.sendfile(__dirname + '/public/index.html');
|
||||
});
|
||||
|
||||
|
||||
var memo_issues = null,
|
||||
memo_milestones = null;
|
||||
|
||||
app.get('/issues.js', function(req, res) {
|
||||
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
|
||||
var all_issues = [];
|
||||
|
||||
function fetchIssues (cb, url, tmpIssues) {
|
||||
|
||||
if(memo_issues) {
|
||||
cb(null, memo_issues);
|
||||
return;
|
||||
}
|
||||
|
||||
var theUrl = url || baseUrl+"/issues?per_page=100&status=open";
|
||||
console.log(theUrl);
|
||||
request.get({
|
||||
url: theUrl,
|
||||
'auth': config.username+":"+config.password
|
||||
}, function (error, response, body) {
|
||||
|
||||
var issues = (tmpIssues ? tmpIssues : []).concat(JSON.parse(body));
|
||||
|
||||
var links = {};
|
||||
response.headers.link.split(', ').forEach(function(headLink){
|
||||
var s = headLink.split('; ');
|
||||
links[s[1]] = s[0].substr(1, s[0].length-2);
|
||||
});
|
||||
|
||||
if(links['rel="next"']) {
|
||||
fetchIssues(cb, links['rel="next"'], issues);
|
||||
}
|
||||
else {
|
||||
memo_issues = issues;
|
||||
cb(null, issues);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
fetchIssues(function(err, issues) {
|
||||
res.send("var issues = "+JSON.stringify(issues)+";");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
app.get('/milestones.js', function(req, res) {
|
||||
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
|
||||
if(memo_milestones) {
|
||||
res.send("var milestones = "+memo_milestones+";");
|
||||
return;
|
||||
}
|
||||
|
||||
request.get({
|
||||
url: baseUrl+"/milestones?per_page=100&status=open",
|
||||
'auth': config.username+":"+config.password
|
||||
}, function (error, response, body) {
|
||||
memo_milestones = body;
|
||||
res.send("var milestones = "+body+";");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
console.log('Listening on port 3000');
|
Загрузка…
Ссылка в новой задаче