1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
5 class MTrack_Milestone {
9 public $description = null;
10 public $duedate = null;
11 public $startdate = null;
12 public $deleted = null;
13 public $completed = null;
14 public $created = null;
16 static function loadByName($name)
18 foreach (MTrackDB::q('select mid from milestones where lower(name) = lower(?)', $name)
19 ->fetchAll() as $row) {
20 return new self($row[0]);
25 static function loadByID($id)
27 foreach (MTrackDB::q('select mid from milestones where mid = ?', $id)
28 ->fetchAll() as $row) {
29 return new self($row[0]);
34 static function enumMilestones($all = false)
37 $q = MTrackDB::q('select mid, name from milestones where deleted != 1');
39 $q = MTrackDB::q('select mid, name from milestones where completed is null and deleted != 1');
42 foreach ($q->fetchAll(PDO::FETCH_NUM) as $row) {
43 $res[$row[0]] = $row[1];
48 function __construct($id = null)
53 list($row) = MTrackDB::q('select * from milestones where mid = ?', $id)
54 ->fetchAll(PDO::FETCH_ASSOC);
55 foreach ($row as $k => $v) {
59 $this->deleted = false;
62 function save(MTrackChangeset $CS)
64 $this->updated = $CS->cid;
66 if ($this->mid === null) {
67 $this->created = $CS->cid;
69 MTrackDB::q('insert into milestones
70 (name, description, startdate, duedate, completed, created,
71 pmid, updated, deleted)
72 values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
83 $this->mid = MTrackDB::lastInsertId('milestones', 'mid');
85 list($old) = MTrackDB::q(
86 'select * from milestones where mid = ?', $this->mid)->fetchAll();
87 foreach ($old as $k => $v) {
88 if ($k == 'mid' || $k == 'created' || $k == 'updated') {
91 $CS->add("milestone:$this->mid:$k", $v, $this->$k);
93 MTrackDB::q('update milestones set name = ?,
94 description = ?, startdate = ?, duedate = ?, completed = ?,
95 updated = ?, deleted = ?, pmid = ?
109 static function macro_BurnDown() {
112 $args = func_get_args();
114 if (!count($args) || (count($args) == 1 && $args[0] == '')) {
115 # Special case for allowing burndown to NOP in the milestone summary
124 foreach ($args as $arg) {
125 list($name, $value) = explode('=', $arg, 2);
126 $params[$name] = $value;
129 $m = MTrack_Milestone::loadByName($params['milestone']);
131 return "BurnDown: milestone $params[milestone] is invalid<br>\n";
133 if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
134 return "Not authorized to view milestone $name<br>\n";
137 /* step 1: find all changes on this milestone and its children */
138 $effort = MTrackDB::q("
139 select expended, remaining, changedate
143 effort e on (tm.tid = e.tid)
145 changes c on (e.cid = c.cid)
147 or (mid in (select mid from milestones where pmid = ?))
149 and c.changedate is not null
150 order by c.changedate",
151 $m->mid, $m->mid)->fetchAll(PDO::FETCH_NUM);
153 /* estimated hours by day */
154 $estimate_by_day = array();
155 /* accumulated work spent by day */
156 $accum_spent_by_day = array();
157 /* accumulated remaining hours by day */
158 $accum_remain_by_day = array();
160 $current_estimate = null;
165 foreach ($effort as $info) {
166 list($exp, $rem, $date) = $info;
167 list($day, $rest) = explode('T', $date, 2);
169 /* previous day estimate carries over to today */
170 if (!isset($estimate_by_day[$day])) {
171 $estimate_by_day[$day] = $current_estimate;
174 /* previous accumulation carries over */
175 if (!isset($accum_spent_by_day[$day])) {
176 $accum_spent_by_day[$day] = $total_exp;
179 /* revise the estimate for today; also applies
180 * to the number we carry over to tomorrow */
182 $estimate_by_day[$day] += $rem;
183 $current_estimate = $estimate_by_day[$day];
187 if ($exp != 0 && $min_day === null) {
188 $min_day = strtotime($date);
190 $accum_spent_by_day[$day] += $exp;
193 $accum_remain_by_day[$day] = $current_estimate - $total_exp;
194 $max_value = max($max_value, $current_estimate);
198 foreach ($estimate_by_day as $v) {
205 /* limit the view to the past 3 weeks */
206 $earliest = strtotime('-3 week');
207 if ($min_day < $earliest) {
208 // $min_day = $earliest;
213 $maxday = strtotime($m->duedate);
217 $maxday = strtotime('1 week', $maxday) * 1000;
219 /* step 3: compute the day by day remaining value,
220 * and produce data series for remaining and expended time */
222 $js_remain = array();
223 $js_estimate = array();
225 foreach ($accum_remain_by_day as $day => $remaining) {
227 /* compute javascript timestamp */
228 list($year, $month, $dayno) = explode('-', $day);
229 $ts = gmmktime(0, 0, 0, $month, $dayno, $year) * 1000;
231 $js_remain[] = "[$ts, $remaining]";
232 $est = (int)$estimate_by_day[$day];
233 $js_estimate[] = "[$ts, $est]";
234 $trend[$ts] = $remaining;
237 $js_remain = join(',', $js_remain);
238 $js_estimate = join(',', $js_estimate);
240 $flot = "bd_graph_" . sha1(join(':', $args) . time());
244 $height = (int)$params['height'];
247 <div id='$flot' class='flotgraph'
248 style='width: $params[width]; height: $params[height];'></div>
249 <script id='source_$flot' language='javascript' type='text/javascript'>
251 var p = \$('#$flot');
252 // Not sure what's up here, but somehow the height for the element
253 // shows up as 0 in safari, despite the style setting above... so let's
254 // just force the height here.
255 if (p.height() == 0) {
259 { label: \"estimated\", data: [$js_estimate], yaxis: 1 },
260 { label: \"remaining\", data: [$js_remain] }
283 $delta = $init_estimate - $total_exp;
286 "<div class='burndown'>Initial estimate: $init_estimate, Work expended: $total_exp<br>\n"
290 static function macro_MilestoneSummary($name) {
293 $m = self::loadByName($name);
295 return "milestone: " . htmlentities($name) . " not found<br>\n";
298 if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
299 return "Not authorized to view milestone $name<br>\n";
302 $completed = mtrack_date($m->completed);
303 $description = $m->description;
304 if (strpos($description, "[[BurnDown(") === false) {
305 $description = "[[BurnDown(milestone=$name,width=50%,height=150)]]\n" .
308 $desc = MTrack_Wiki::format_to_html($description);
310 if ($m->completed !== NULL) {
311 $pname = "<del>$name</del>";
313 } elseif ($m->duedate) {
314 $due = "Due " . mtrack_date($m->duedate);
319 $watch = MTrackWatch::getWatchUI('milestone', $m->mid);
322 <div class="milestone">
323 <h2><a href="{$ABSWEB}milestone.php/$name">$pname</a></h2>
325 <div class="due">$due</div>
334 foreach (MTrackDB::q('select status, estimated, estimated - spent as remaining from ticket_milestones tm left join tickets t on (tm.tid = t.tid) where mid = ?',
335 $m->mid)->fetchAll(PDO::FETCH_ASSOC) as $row) {
337 if ($row['status'] != 'closed') {
340 $estimated += $row['estimated'];
341 $remaining += $row['remaining'];
344 $closed = $total - $open;
346 $apct = (int)($open / $total * 100);
352 <table class='progress'>
354 <td class='closed' style='width:$cpct%;'><a href='#'></a></td>
359 <td class='open' style='width:$apct%;'><a href='#'> </a></td>
363 $ms = urlencode($name);
368 <a href='{$ABSWEB}query.php?milestone=$ms&status!=closed'>$open open</a>,
369 <a href='{$ABSWEB}query.php?milestone=$ms&status=closed'>$closed closed</a>,
370 <a href='{$ABSWEB}query.php?milestone=$ms'>$total total</a> ($cpct % complete)