2 require_once 'MTrack/Interface/IssueListener.php';
3 require_once 'MTrack/DB.php';
4 require_once 'MTrack/Config.php';
5 require_once 'MTrack/Component.php';
6 require_once 'MTrack/Wiki.php';
7 require_once 'MTrack/Changeset.php';
8 require_once 'MTrack/SearchDB.php';
9 require_once 'MTrack/Milestone.php';
10 require_once 'MTrack/Keyword.php';
15 public $nsident = null;
16 public $summary = null;
17 public $description = null;
18 public $created = null;
19 public $updated = null;
21 public $priority = null;
22 public $severity = null;
23 public $classification = null;
24 public $resolution = null;
25 public $status = null;
26 public $estimated = null;
28 public $changelog = null;
30 protected $components = null;
31 protected $origcomponents = null;
32 protected $milestones = null;
33 protected $origmilestones = null;
34 protected $comments_to_add = array();
35 protected $keywords = null;
36 protected $origkeywords = null;
37 protected $effort = array();
39 static $_listeners = array();
40 static function loadById($id) {
42 return new MTrackIssue($id);
43 } catch (Exception $e) {
47 static function loadByNSIdent($id) // really the integer id.
49 static $cache = array();
50 if (!isset($cache[$id])) {
51 $ids = MTrackDB::q('select tid from tickets where nsident = ?', $id)
52 ->fetchAll(PDO::FETCH_COLUMN, 0);
53 if (count($ids) == 1) {
54 $cache[$id] = $ids[0];
59 return new MTrackIssue($cache[$id]);
61 static function registerListener(IMTrackIssueListener $l) // used by CustomFields
63 self::$_listeners[] = $l;
65 static function index_issue($object)
67 list($ignore, $ident) = explode(':', $object, 2);
68 $i = MTrackIssue::loadById($ident);
70 echo "Ticket #$i->nsident\n";
72 $CS = MTrackChangeset::get($i->updated);
73 $CSC = MTrackChangeset::get($i->created);
75 $kw = join(' ', array_values($i->getKeywords()));
77 'summary' => $i->summary,
78 'description' => $i->description,
79 'changelog' => $i->changelog,
81 'stored:date' => $CS->when,
83 'creator' => $CSC->who,
84 'stored:created' => $CSC->when,
87 $i->augmentIndexerFields($idx);
88 MTrackSearchDB::add("ticket:$i->tid", $idx, true);
90 foreach (MTrackDB::q('select value, changedate, who from
91 change_audit left join changes using (cid) where fieldname = ?',
92 "ticket:$ident:@comment") as $row) {
93 list($text, $when, $who) = $row;
96 $elapsed = time() - $start;
98 echo " - comment $who $when took $elapsed to hash\n";
101 if (strlen($text) > 8192) {
102 // A huge paste into a ticket
103 $text = substr($text, 0, 8192);
105 MTrackSearchDB::add("ticket:$ident:comment:$id", array(
106 'description' => $text,
107 'stored:date' => $when,
111 $elapsed = time() - $start;
113 echo " - comment $who $when took $elapsed to index\n";
120 function __construct($tid = null) {
122 $this->components = array();
123 $this->origcomponents = array();
124 $this->milestones = array();
125 $this->origmilestones = array();
126 $this->keywords = array();
127 $this->origkeywords = array();
128 $this->status = 'new';
130 foreach (array('classification', 'severity', 'priority') as $f) {
131 $this->$f = MTrackConfig::get('ticket', "default.$f");
134 $data = MTrackDB::q('select * from tickets where tid = ?',
138 if (isset($data[0])) {
142 if (!is_array($row)) {
143 throw new Exception("no such issue $tid");
146 foreach ($row as $k => $v) {
152 function applyPOSTData($data) {
153 foreach (self::$_listeners as $l) {
154 $l->applyPOSTData($this, $data);
158 function augmentFormFields(&$FIELDSET) {
159 foreach (self::$_listeners as $l) {
160 $l->augmentFormFields($this, $FIELDSET);
163 function augmentIndexerFields(&$idx) {
164 foreach (self::$_listeners as $l) {
165 $l->augmentIndexerFields($this, $idx);
168 function augmentSaveParams(&$params) {
169 foreach (self::$_listeners as $l) {
170 $l->augmentSaveParams($this, $params);
176 $args = func_get_args();
177 $method = array_shift($args);
180 foreach (self::$_listeners as $l) {
181 $v = call_user_func_array(array($l, $method), $args);
188 foreach ($veto as $r) {
197 require_once 'Exception/Veto.php';
198 throw new MTrackVetoException($reasons);
202 function save(MTrackChangeset $CS)
204 $db = MTrackDB::get();
207 if ($this->tid === null) {
208 $this->created = $CS->cid;
212 list($oldrow) = MTrackDB::q('select * from tickets where tid = ?',
213 $this->tid)->fetchAll();
216 $this->checkVeto('vetoSave', $this, $oldrow);
218 $this->updated = $CS->cid;
221 'summary' => $this->summary,
222 'description' => $this->description,
223 'created' => $this->created,
224 'updated' => $this->updated,
225 'owner' => $this->owner,
226 'changelog' => $this->changelog,
227 'priority' => $this->priority,
228 'severity' => $this->severity,
229 'classification' => $this->classification,
230 'resolution' => $this->resolution,
231 'status' => $this->status,
232 'estimated' => (float)$this->estimated,
233 'spent' => (float)$this->spent,
234 'nsident' => $this->nsident,
238 $this->augmentSaveParams($params);
240 if ($this->tid === null) {
241 $sql = 'insert into tickets ';
245 require_once 'UUID.php';
246 $new_tid = new OmniTI_Util_UUID;
247 $new_tid = $new_tid->toRFC4122String(false);
250 $values[] = "'$new_tid'";
252 foreach ($params as $key => $value) {
257 $sql .= "(" . join(', ', $keys) . ") values (" .
258 join(', ', $values) . ")";
260 $sql = 'update tickets set ';
262 foreach ($params as $key => $value) {
263 $values[] = "$key = :$key";
265 $sql .= join(', ', $values) . " where tid = :tid";
267 $params['tid'] = $this->tid;
270 $q = $db->prepare($sql);
271 $q->execute($params);
273 if ($this->tid === null) {
274 $this->tid = $new_tid;
280 foreach ($params as $key => $value) {
281 if ($key == 'created' || $key == 'updated' || $key == 'tid') {
284 if ($key == 'changelog' || $key == 'description' || $key == 'summary') {
285 if (!isset($oldrow[$key]) || $oldrow[$key] != $value) {
289 if (!isset($oldrow[$key])) {
290 $oldrow[$key] = null;
292 $CS->add("ticket:$this->tid:$key", $oldrow[$key], $value);
295 $this->compute_diff($CS, 'components', 'ticket_components', 'compid',
296 $this->components, $this->origcomponents);
297 $this->compute_diff($CS, 'keywords', 'ticket_keywords', 'kid',
298 $this->keywords, $this->origkeywords);
299 $this->compute_diff($CS, 'milestones', 'ticket_milestones', 'mid',
300 $this->milestones, $this->origmilestones);
302 foreach ($this->comments_to_add as $text) {
303 $CS->add("ticket:$this->tid:@comment", null, $text);
306 foreach ($this->effort as $effort) {
307 MTrackDB::q('insert into effort (tid, cid, expended, remaining)
308 values (?, ?, ?, ?)',
309 $this->tid, $CS->cid, $effort[0], $effort[1]);
311 $this->effort = array();
314 function getComponents()
316 if ($this->components !== null) {
317 return $this->components ;
320 select tc.compid, name
321 from ticket_components tc
322 left join components using (compid)
323 where tid = ?', $this->tid);
325 $this->origcomponents = array();
326 foreach ($q->fetchAll() as $row) {
327 $this->origcomponents[$row[0]] = $row[1];
329 $this->components = $this->origcomponents;
331 return $this->components;
334 function assocComponent($comp)
336 $comp = $this->resolveComponent($comp);
337 $this->getComponents();
338 $this->checkVeto('vetoComponent', $this, $comp, true);
339 $this->components[$comp->compid] = $comp->name;
342 function dissocComponent($comp)
344 $comp = $this->resolveComponent($comp);
345 $this->getComponents();
346 $this->checkVeto('vetoComponent', $this, $comp, false);
347 unset($this->components[$comp->compid]);
350 function getMilestones()
352 if ($this->milestones === null) {
353 $comps = MTrackDB::q('select tc.mid, name from ticket_milestones tc left join milestones using (mid) where tid = ? order by duedate, name', $this->tid)->fetchAll();
354 $this->origmilestones = array();
355 foreach ($comps as $row) {
356 $this->origmilestones[$row[0]] = $row[1];
358 $this->milestones = $this->origmilestones;
360 return $this->milestones;
364 function assocMilestone($M)
366 $ms = $this->resolveMilestone($M);
368 throw new Exception("unable to resolve milestone $M");
370 $this->getMilestones();
371 $this->checkVeto('vetoMilestone', $this, $ms, true);
372 $this->milestones[$ms->mid] = $ms->name;
375 function dissocMilestone($M)
377 $ms = $this->resolveMilestone($M);
379 throw new Exception("unable to resolve milestone $M");
381 $this->getMilestones();
382 $this->checkVeto('vetoMilestone', $this, $ms, false);
383 unset($this->milestones[$ms->mid]);
386 function addComment($comment)
388 $comment = trim($comment);
389 if (strlen($comment)) {
390 $this->checkVeto('vetoComment', $this, $comment);
391 $this->comments_to_add[] = $comment;
396 function assocKeyword($kw)
398 $kw = $this->resolveKeyword($kw);
399 $this->getKeywords();
400 $this->checkVeto('vetoKeyword', $this, $kw, true);
401 $this->keywords[$kw->kid] = $kw->keyword;
404 function dissocKeyword($kw)
406 $kw = $this->resolveKeyword($kw);
407 $this->getKeywords();
408 $this->checkVeto('vetoKeyword', $this, $kw, false);
409 unset($this->keywords[$kw->kid]);
412 function getKeywords()
414 if ($this->keywords === null) {
415 $comps = MTrackDB::q('select tc.kid, keyword from ticket_keywords tc left join keywords using (kid) where tid = ?', $this->tid)->fetchAll();
416 $this->origkeywords = array();
417 foreach ($comps as $row) {
418 $this->origkeywords[$row[0]] = $row[1];
420 $this->keywords = $this->origkeywords;
422 return $this->keywords;
425 function addEffort($amount, $revised = null)
428 if ($revised !== null) {
429 $diff = $revised - $this->estimated;
430 $this->estimated = $revised;
432 $this->effort[] = array($amount, $diff);
433 $this->spent += $amount;
438 $this->status = 'closed';
439 $this->addEffort(0, 0);
444 switch ($this->status) {
454 $this->status = 'reopened';
455 $this->resolution = null;
461 $ret = get_object_vars($this);
462 // echo '<PRE>'; print_r($ret);exit;
470 function updateWho($link)
472 return $link->username(
474 MTrackChangeset::get($this->updated)->who :
479 function updateWhen($link)
481 return $link->date( $this->updated ?
482 MTrackChangeset::get($this->updated)->when :
483 MTrackDB::unixtime(time())
486 function createdWho($link)
488 return $link->username(
490 MTrackChangeset::get($this->created)->who :
495 function createdWhen($link)
497 return $link->date( $this->created ?
498 MTrackChangeset::get($this->created)->when :
499 MTrackDB::unixtime(time())
503 function keywordsToHtml()
506 foreach ($this->getKeywords() as $kw) {
507 $value[] = mtrack_keyword($kw);
509 return join(' ', $value);
514 function componentsToHtml()
517 $value = join(',',array_keys($this->getComponents()));
519 if (!strlen($value)) {
523 "select name, deleted from components where compid in ($value)");
524 foreach ($q->fetchAll() as $row) {
526 $c = ($row['deleted'] ? '<del>' : '') .
527 htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
528 ($row['deleted'] ? '</del>' : '');
532 return join(", ", $res);
536 function milestonesToHtml($msurl)
538 if (empty($this->milestone_url)) {
539 die("requires issue->milestone_url to be set.");
542 $value = join(',',array_keys($this->getMilestones()));
544 if (!strlen($value)) {
547 foreach (MTrackDB::q(
548 "select name, completed, deleted from milestones where mid in ($value)")
549 ->fetchAll() as $row) {
551 $row['deleted'] = strlen($row['completed']) ? 1 : 0;
553 $c = "<span class='milestone" .
554 ($row['deleted'] ? " completed" : '') .
555 "'><a href=\"{$this->milestone_url}" . urlencode($row['name']) . '">' .
556 htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
560 return join(", ", $res);
562 function descriptionToHtml()
564 return MTrack_Wiki::format_to_html($this->description);
566 /* function attachmentsToHtml()
568 require_once 'Attachment.php';
569 return MTrackAttachment::renderList("ticket:{$this->tid}");
571 function attachmentsDeleteToHtml() {
572 require_once 'Attachment.php';
573 return MTrackAttachment::renderDeleteList("ticket:{$this->tid}");
576 function toIdString() {
577 return "ticket:{$this->tid}";
582 function watcherButton()
584 return MTrackWatch::renderWatchUI('ticket', $this->tid);
598 return MTrackWatch::objectWatchers($this);
602 private function resolveMilestone($ms)
604 if ($ms instanceof MTrack_Milestone) {
607 if (ctype_digit($ms)) {
608 return MTrack_Milestone::loadById($ms);
610 return MTrack_Milestone::loadByName($ms);
613 private function resolveKeyword($kw)
615 if ($kw instanceof MTrackKeyword) {
618 $k = MTrackKeyword::loadByWord($kw);
620 if (ctype_digit($kw)) {
621 return MTrackKeyword::loadById($kw);
623 throw new Exception("unknown keyword $kw");
627 private function compute_diff(MTrackChangeset $CS, $label,
628 $tablename, $keyname, $current, $orig) {
629 if (!is_array($current)) {
632 if (!is_array($orig)) {
635 $added = array_keys(array_diff_key($current, $orig));
636 $removed = array_keys(array_diff_key($orig, $current));
638 $db = MTrackDB::get();
640 "insert into $tablename (tid, $keyname) values (?, ?)");
642 "delete from $tablename where tid = ? AND $keyname = ?");
643 foreach ($added as $key) {
644 $ADD->execute(array($this->tid, $key));
646 foreach ($removed as $key) {
647 $DEL->execute(array($this->tid, $key));
649 if (count($added) + count($removed)) {
650 $old = join(',', array_keys($orig));
651 $new = join(',', array_keys($current));
653 "ticket:$this->tid:@$label", $old, $new);
656 private function resolveComponent($comp)
658 if ($comp instanceof MTrackComponent) {
661 if (ctype_digit($comp)) {
662 return MTrackComponent::loadById($comp);
664 return MTrackComponent::loadByName($comp);