1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 require_once 'MTrack/Wiki.php';
5 require_once 'MTrack/ACL.php';
6 require_once 'MTrack/Watch.php';
9 class MTrack_Milestone {
13 public $description = null;
14 public $duedate = null;
15 public $startdate = null;
16 public $deleted = null;
17 public $completed = null;
18 public $created = null;
20 static function loadByName($name)
22 foreach (MTrackDB::q('select mid from milestones where lower(name) = lower(?)', $name)
23 ->fetchAll() as $row) {
24 return new self($row[0]);
29 static function loadByID($id)
31 foreach (MTrackDB::q('select mid from milestones where mid = ?', $id)
32 ->fetchAll() as $row) {
33 return new self($row[0]);
38 static function enumMilestones($all = false)
41 $q = MTrackDB::q('select mid, name from milestones where deleted != 1');
43 $q = MTrackDB::q('select mid, name from milestones where completed is null and deleted != 1');
46 foreach ($q->fetchAll(PDO::FETCH_NUM) as $row) {
47 $res[$row[0]] = $row[1];
52 function __construct($id = null)
57 list($row) = MTrackDB::q('select * from milestones where mid = ?', $id)
58 ->fetchAll(PDO::FETCH_ASSOC);
59 foreach ($row as $k => $v) {
63 $this->deleted = false;
66 function save(MTrackChangeset $CS)
68 $this->updated = $CS->cid;
70 if ($this->mid === null) {
71 $this->created = $CS->cid;
73 MTrackDB::q('insert into milestones
74 (name, description, startdate, duedate, completed, created,
75 pmid, updated, deleted)
76 values (?, ?, ?, ?, ?, ?, ?, ?, ?)',
87 $this->mid = MTrackDB::lastInsertId('milestones', 'mid');
89 list($old) = MTrackDB::q(
90 'select * from milestones where mid = ?', $this->mid)->fetchAll();
91 foreach ($old as $k => $v) {
92 if ($k == 'mid' || $k == 'created' || $k == 'updated') {
95 $CS->add("milestone:$this->mid:$k", $v, $this->$k);
97 MTrackDB::q('update milestones set name = ?,
98 description = ?, startdate = ?, duedate = ?, completed = ?,
99 updated = ?, deleted = ?, pmid = ?
113 static function macro_BurnDown() {
116 $args = func_get_args();
118 if (!count($args) || (count($args) == 1 && $args[0] == '')) {
119 # Special case for allowing burndown to NOP in the milestone summary
128 foreach ($args as $arg) {
129 list($name, $value) = explode('=', $arg, 2);
130 $params[$name] = $value;
133 $m = MTrack_Milestone::loadByName($params['milestone']);
135 return "BurnDown: milestone $params[milestone] is invalid<br>\n";
137 if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
138 return "Not authorized to view milestone $name<br>\n";
141 /* step 1: find all changes on this milestone and its children */
142 $effort = MTrackDB::q("
143 select expended, remaining, changedate
147 effort e on (tm.tid = e.tid)
149 changes c on (e.cid = c.cid)
151 or (mid in (select mid from milestones where pmid = ?))
153 and c.changedate is not null
154 order by c.changedate",
155 $m->mid, $m->mid)->fetchAll(PDO::FETCH_NUM);
157 /* estimated hours by day */
158 $estimate_by_day = array();
159 /* accumulated work spent by day */
160 $accum_spent_by_day = array();
161 /* accumulated remaining hours by day */
162 $accum_remain_by_day = array();
164 $current_estimate = null;
169 foreach ($effort as $info) {
170 list($exp, $rem, $date) = $info;
171 list($day, $rest) = explode('T', $date, 2);
173 /* previous day estimate carries over to today */
174 if (!isset($estimate_by_day[$day])) {
175 $estimate_by_day[$day] = $current_estimate;
178 /* previous accumulation carries over */
179 if (!isset($accum_spent_by_day[$day])) {
180 $accum_spent_by_day[$day] = $total_exp;
183 /* revise the estimate for today; also applies
184 * to the number we carry over to tomorrow */
186 $estimate_by_day[$day] += $rem;
187 $current_estimate = $estimate_by_day[$day];
191 if ($exp != 0 && $min_day === null) {
192 $min_day = strtotime($date);
194 $accum_spent_by_day[$day] += $exp;
197 $accum_remain_by_day[$day] = $current_estimate - $total_exp;
198 $max_value = max($max_value, $current_estimate);
202 foreach ($estimate_by_day as $v) {
209 /* limit the view to the past 3 weeks */
210 $earliest = strtotime('-3 week');
211 if ($min_day < $earliest) {
212 // $min_day = $earliest;
217 $maxday = strtotime($m->duedate);
221 $maxday = strtotime('1 week', $maxday) * 1000;
223 /* step 3: compute the day by day remaining value,
224 * and produce data series for remaining and expended time */
226 $js_remain = array();
227 $js_estimate = array();
229 foreach ($accum_remain_by_day as $day => $remaining) {
231 /* compute javascript timestamp */
232 list($year, $month, $dayno) = explode('-', $day);
233 $ts = gmmktime(0, 0, 0, $month, $dayno, $year) * 1000;
235 $js_remain[] = "[$ts, $remaining]";
236 $est = (int)$estimate_by_day[$day];
237 $js_estimate[] = "[$ts, $est]";
238 $trend[$ts] = $remaining;
241 $js_remain = join(',', $js_remain);
242 $js_estimate = join(',', $js_estimate);
244 $flot = "bd_graph_" . sha1(join(':', $args) . time());
248 $height = (int)$params['height'];
251 <div id='$flot' class='flotgraph'
252 style='width: $params[width]; height: $params[height];'></div>
253 <script id='source_$flot' language='javascript' type='text/javascript'>
255 var p = \$('#$flot');
256 // Not sure what's up here, but somehow the height for the element
257 // shows up as 0 in safari, despite the style setting above... so let's
258 // just force the height here.
259 if (p.height() == 0) {
263 { label: \"estimated\", data: [$js_estimate], yaxis: 1 },
264 { label: \"remaining\", data: [$js_remain] }
287 $delta = $init_estimate - $total_exp;
290 "<div class='burndown'>Initial estimate: $init_estimate, Work expended: $total_exp<br>\n"
294 static function macro_MilestoneSummary($name) {
297 $m = self::loadByName($name);
299 return "milestone: " . htmlentities($name) . " not found<br>\n";
302 if (!MTrackACL::hasAllRights("milestone:" . $m->mid, 'read')) {
303 return "Not authorized to view milestone $name<br>\n";
306 $completed = mtrack_date($m->completed);
307 $description = $m->description;
308 if (strpos($description, "[[BurnDown(") === false) {
309 $description = "[[BurnDown(milestone=$name,width=50%,height=150)]]\n" .
312 $desc = MTrack_Wiki::format_to_html($description);
314 if ($m->completed !== NULL) {
315 $pname = "<del>$name</del>";
317 } elseif ($m->duedate) {
318 $due = "Due " . mtrack_date($m->duedate);
323 $watch = MTrackWatch::getWatchUI('milestone', $m->mid);
326 <div class="milestone">
327 <h2><a href="{$ABSWEB}milestone.php/$name">$pname</a></h2>
329 <div class="due">$due</div>
338 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 = ?',
339 $m->mid)->fetchAll(PDO::FETCH_ASSOC) as $row) {
341 if ($row['status'] != 'closed') {
344 $estimated += $row['estimated'];
345 $remaining += $row['remaining'];
348 $closed = $total - $open;
350 $apct = (int)($open / $total * 100);
356 <table class='progress'>
358 <td class='closed' style='width:$cpct%;'><a href='#'></a></td>
363 <td class='open' style='width:$apct%;'><a href='#'> </a></td>
367 $ms = urlencode($name);
372 <a href='{$ABSWEB}query.php?milestone=$ms&status!=closed'>$open open</a>,
373 <a href='{$ABSWEB}query.php?milestone=$ms&status=closed'>$closed closed</a>,
374 <a href='{$ABSWEB}query.php?milestone=$ms'>$total total</a> ($cpct % complete)