3 throw new Exception("disabled");
5 require_once 'MTrack/Interface/IssueListener.php';
6 require_once 'MTrack/DB.php';
7 require_once 'MTrack/Config.php';
8 require_once 'MTrack/Component.php';
9 require_once 'MTrack/Wiki.php';
10 //require_once 'MTrack/Changeset.php';
11 require_once 'MTrack/SearchDB.php';
12 require_once 'MTrack/Milestone.php';
13 require_once 'MTrack/Keyword.php';
18 public $nsident = null;
19 public $summary = null;
20 public $description = null;
21 public $created = null;
22 public $updated = null;
24 public $priority = null;
25 public $severity = null;
26 public $classification = null;
27 public $resolution = null;
28 public $status = null;
29 public $estimated = null;
31 public $changelog = null;
33 protected $components = null;
34 protected $origcomponents = null;
35 protected $milestones = null;
36 protected $origmilestones = null;
37 protected $comments_to_add = array();
38 protected $keywords = null;
39 protected $origkeywords = null;
40 protected $effort = array();
42 static $_listeners = array();
43 static function loadById($id) {
45 return new MTrackIssue($id);
46 } catch (Exception $e) {
50 static function loadByNSIdent($id) // really the integer id.
52 static $cache = array();
53 if (!isset($cache[$id])) {
54 $ids = MTrackDB::q('select tid from tickets where nsident = ?', $id)
55 ->fetchAll(PDO::FETCH_COLUMN, 0);
56 if (count($ids) == 1) {
57 $cache[$id] = $ids[0];
62 return new MTrackIssue($cache[$id]);
64 static function registerListener(IMTrackIssueListener $l) // used by CustomFields
66 self::$_listeners[] = $l;
68 static function index_issue($object)
70 list($ignore, $ident) = explode(':', $object, 2);
71 $i = MTrackIssue::loadById($ident);
73 echo "Ticket #$i->nsident\n";
75 $CS = MTrackChangeset::get($i->updated);
76 $CSC = MTrackChangeset::get($i->created);
78 $kw = join(' ', array_values($i->getKeywords()));
80 'summary' => $i->summary,
81 'description' => $i->description,
82 'changelog' => $i->changelog,
84 'stored:date' => $CS->when,
86 'creator' => $CSC->who,
87 'stored:created' => $CSC->when,
90 $i->augmentIndexerFields($idx);
91 MTrackSearchDB::add("ticket:$i->tid", $idx, true);
93 foreach (MTrackDB::q('select value, changedate, who from
94 change_audit left join changes using (cid) where fieldname = ?',
95 "ticket:$ident:@comment") as $row) {
96 list($text, $when, $who) = $row;
99 $elapsed = time() - $start;
101 echo " - comment $who $when took $elapsed to hash\n";
104 if (strlen($text) > 8192) {
105 // A huge paste into a ticket
106 $text = substr($text, 0, 8192);
108 MTrackSearchDB::add("ticket:$ident:comment:$id", array(
109 'description' => $text,
110 'stored:date' => $when,
114 $elapsed = time() - $start;
116 echo " - comment $who $when took $elapsed to index\n";
123 function __construct($tid = null) {
125 $this->components = array();
126 $this->origcomponents = array();
127 $this->milestones = array();
128 $this->origmilestones = array();
129 $this->keywords = array();
130 $this->origkeywords = array();
131 $this->status = 'new';
133 foreach (array('classification', 'severity', 'priority') as $f) {
134 $this->$f = MTrackConfig::get('ticket', "default.$f");
137 $data = MTrackDB::q('select * from tickets where tid = ?',
141 if (isset($data[0])) {
145 if (!is_array($row)) {
146 throw new Exception("no such issue $tid");
149 foreach ($row as $k => $v) {
155 function applyPOSTData($data) {
156 foreach (self::$_listeners as $l) {
157 $l->applyPOSTData($this, $data);
161 function augmentFormFields(&$FIELDSET) {
162 foreach (self::$_listeners as $l) {
163 $l->augmentFormFields($this, $FIELDSET);
166 function augmentIndexerFields(&$idx) {
167 foreach (self::$_listeners as $l) {
168 $l->augmentIndexerFields($this, $idx);
171 function augmentSaveParams(&$params) {
172 foreach (self::$_listeners as $l) {
173 $l->augmentSaveParams($this, $params);
179 $args = func_get_args();
180 $method = array_shift($args);
183 foreach (self::$_listeners as $l) {
184 $v = call_user_func_array(array($l, $method), $args);
191 foreach ($veto as $r) {
200 require_once 'Exception/Veto.php';
201 throw new MTrackVetoException($reasons);
205 function save(MTrackChangeset $CS)
207 $db = MTrackDB::get();
210 if ($this->tid === null) {
211 $this->created = $CS->cid;
215 list($oldrow) = MTrackDB::q('select * from tickets where tid = ?',
216 $this->tid)->fetchAll();
219 $this->checkVeto('vetoSave', $this, $oldrow);
221 $this->updated = $CS->cid;
224 'summary' => $this->summary,
225 'description' => $this->description,
226 'created' => $this->created,
227 'updated' => $this->updated,
228 'owner' => $this->owner,
229 'changelog' => $this->changelog,
230 'priority' => $this->priority,
231 'severity' => $this->severity,
232 'classification' => $this->classification,
233 'resolution' => $this->resolution,
234 'status' => $this->status,
235 'estimated' => (float)$this->estimated,
236 'spent' => (float)$this->spent,
237 'nsident' => $this->nsident,
241 $this->augmentSaveParams($params);
243 if ($this->tid === null) {
244 $sql = 'insert into tickets ';
248 require_once 'UUID.php';
249 $new_tid = new OmniTI_Util_UUID;
250 $new_tid = $new_tid->toRFC4122String(false);
253 $values[] = "'$new_tid'";
255 foreach ($params as $key => $value) {
260 $sql .= "(" . join(', ', $keys) . ") values (" .
261 join(', ', $values) . ")";
263 $sql = 'update tickets set ';
265 foreach ($params as $key => $value) {
266 $values[] = "$key = :$key";
268 $sql .= join(', ', $values) . " where tid = :tid";
270 $params['tid'] = $this->tid;
273 $q = $db->prepare($sql);
274 $q->execute($params);
276 if ($this->tid === null) {
277 $this->tid = $new_tid;
283 foreach ($params as $key => $value) {
284 if ($key == 'created' || $key == 'updated' || $key == 'tid') {
287 if ($key == 'changelog' || $key == 'description' || $key == 'summary') {
288 if (!isset($oldrow[$key]) || $oldrow[$key] != $value) {
292 if (!isset($oldrow[$key])) {
293 $oldrow[$key] = null;
295 $CS->add("ticket:$this->tid:$key", $oldrow[$key], $value);
298 $this->compute_diff($CS, 'components', 'ticket_components', 'compid',
299 $this->components, $this->origcomponents);
300 $this->compute_diff($CS, 'keywords', 'ticket_keywords', 'kid',
301 $this->keywords, $this->origkeywords);
302 $this->compute_diff($CS, 'milestones', 'ticket_milestones', 'mid',
303 $this->milestones, $this->origmilestones);
305 foreach ($this->comments_to_add as $text) {
306 $CS->add("ticket:$this->tid:@comment", null, $text);
309 foreach ($this->effort as $effort) {
310 MTrackDB::q('insert into effort (tid, cid, expended, remaining)
311 values (?, ?, ?, ?)',
312 $this->tid, $CS->cid, $effort[0], $effort[1]);
314 $this->effort = array();
317 function getComponents()
319 if ($this->components !== null) {
320 return $this->components ;
323 select tc.compid, name
324 from ticket_components tc
325 left join components using (compid)
326 where tid = ?', $this->tid);
328 $this->origcomponents = array();
329 foreach ($q->fetchAll() as $row) {
330 $this->origcomponents[$row[0]] = $row[1];
332 $this->components = $this->origcomponents;
334 return $this->components;
337 function assocComponent($comp)
339 $comp = $this->resolveComponent($comp);
340 $this->getComponents();
341 $this->checkVeto('vetoComponent', $this, $comp, true);
342 $this->components[$comp->compid] = $comp->name;
345 function dissocComponent($comp)
347 $comp = $this->resolveComponent($comp);
348 $this->getComponents();
349 $this->checkVeto('vetoComponent', $this, $comp, false);
350 unset($this->components[$comp->compid]);
353 function getMilestones()
355 if ($this->milestones === null) {
356 $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();
357 $this->origmilestones = array();
358 foreach ($comps as $row) {
359 $this->origmilestones[$row[0]] = $row[1];
361 $this->milestones = $this->origmilestones;
363 return $this->milestones;
367 function assocMilestone($M)
369 $ms = $this->resolveMilestone($M);
371 throw new Exception("unable to resolve milestone $M");
373 $this->getMilestones();
374 $this->checkVeto('vetoMilestone', $this, $ms, true);
375 $this->milestones[$ms->mid] = $ms->name;
378 function dissocMilestone($M)
380 $ms = $this->resolveMilestone($M);
382 throw new Exception("unable to resolve milestone $M");
384 $this->getMilestones();
385 $this->checkVeto('vetoMilestone', $this, $ms, false);
386 unset($this->milestones[$ms->mid]);
389 function addComment($comment)
391 $comment = trim($comment);
392 if (strlen($comment)) {
393 $this->checkVeto('vetoComment', $this, $comment);
394 $this->comments_to_add[] = $comment;
399 function assocKeyword($kw)
401 $kw = $this->resolveKeyword($kw);
402 $this->getKeywords();
403 $this->checkVeto('vetoKeyword', $this, $kw, true);
404 $this->keywords[$kw->kid] = $kw->keyword;
407 function dissocKeyword($kw)
409 $kw = $this->resolveKeyword($kw);
410 $this->getKeywords();
411 $this->checkVeto('vetoKeyword', $this, $kw, false);
412 unset($this->keywords[$kw->kid]);
415 function getKeywords()
417 if ($this->keywords === null) {
418 $comps = MTrackDB::q('select tc.kid, keyword from ticket_keywords tc left join keywords using (kid) where tid = ?', $this->tid)->fetchAll();
419 $this->origkeywords = array();
420 foreach ($comps as $row) {
421 $this->origkeywords[$row[0]] = $row[1];
423 $this->keywords = $this->origkeywords;
425 return $this->keywords;
428 function addEffort($amount, $revised = null)
431 if ($revised !== null) {
432 $diff = $revised - $this->estimated;
433 $this->estimated = $revised;
435 $this->effort[] = array($amount, $diff);
436 $this->spent += $amount;
441 $this->status = 'closed';
442 $this->addEffort(0, 0);
447 switch ($this->status) {
457 $this->status = 'reopened';
458 $this->resolution = null;
464 $ret = get_object_vars($this);
465 // echo '<PRE>'; print_r($ret);exit;
473 function updateWho($link)
475 return $link->username(
477 MTrackChangeset::get($this->updated)->who :
482 function updateWhen($link)
484 return $link->date( $this->updated ?
485 MTrackChangeset::get($this->updated)->when :
486 MTrackDB::unixtime(time())
489 function createdWho($link)
491 return $link->username(
493 MTrackChangeset::get($this->created)->who :
498 function createdWhen($link)
500 return $link->date( $this->created ?
501 MTrackChangeset::get($this->created)->when :
502 MTrackDB::unixtime(time())
506 function keywordsToHtml()
509 foreach ($this->getKeywords() as $kw) {
510 $value[] = mtrack_keyword($kw);
512 return join(' ', $value);
517 function componentsToHtml()
520 $value = join(',',array_keys($this->getComponents()));
522 if (!strlen($value)) {
526 "select name, deleted from components where compid in ($value)");
527 foreach ($q->fetchAll() as $row) {
529 $c = ($row['deleted'] ? '<del>' : '') .
530 htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
531 ($row['deleted'] ? '</del>' : '');
535 return join(", ", $res);
539 function milestonesToHtml($msurl)
541 if (empty($this->milestone_url)) {
542 die("requires issue->milestone_url to be set.");
545 $value = join(',',array_keys($this->getMilestones()));
547 if (!strlen($value)) {
550 foreach (MTrackDB::q(
551 "select name, completed, deleted from milestones where mid in ($value)")
552 ->fetchAll() as $row) {
554 $row['deleted'] = strlen($row['completed']) ? 1 : 0;
556 $c = "<span class='milestone" .
557 ($row['deleted'] ? " completed" : '') .
558 "'><a href=\"{$this->milestone_url}" . urlencode($row['name']) . '">' .
559 htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
563 return join(", ", $res);
565 function descriptionToHtml()
567 return MTrack_Wiki::format_to_html($this->description);
569 /* function attachmentsToHtml()
571 require_once 'Attachment.php';
572 return MTrackAttachment::renderList("ticket:{$this->tid}");
574 function attachmentsDeleteToHtml() {
575 require_once 'Attachment.php';
576 return MTrackAttachment::renderDeleteList("ticket:{$this->tid}");
579 function toIdString() {
580 return "ticket:{$this->tid}";
585 function watcherButton()
587 return MTrackWatch::renderWatchUI('ticket', $this->tid);
601 return MTrackWatch::objectWatchers($this);
605 private function resolveMilestone($ms)
607 if ($ms instanceof MTrack_Milestone) {
610 if (ctype_digit($ms)) {
611 return MTrack_Milestone::loadById($ms);
613 return MTrack_Milestone::loadByName($ms);
616 private function resolveKeyword($kw)
618 if ($kw instanceof MTrackKeyword) {
621 $k = MTrackKeyword::loadByWord($kw);
623 if (ctype_digit($kw)) {
624 return MTrackKeyword::loadById($kw);
626 throw new Exception("unknown keyword $kw");
630 private function compute_diff(MTrackChangeset $CS, $label,
631 $tablename, $keyname, $current, $orig) {
632 if (!is_array($current)) {
635 if (!is_array($orig)) {
638 $added = array_keys(array_diff_key($current, $orig));
639 $removed = array_keys(array_diff_key($orig, $current));
641 $db = MTrackDB::get();
643 "insert into $tablename (tid, $keyname) values (?, ?)");
645 "delete from $tablename where tid = ? AND $keyname = ?");
646 foreach ($added as $key) {
647 $ADD->execute(array($this->tid, $key));
649 foreach ($removed as $key) {
650 $DEL->execute(array($this->tid, $key));
652 if (count($added) + count($removed)) {
653 $old = join(',', array_keys($orig));
654 $new = join(',', array_keys($current));
656 "ticket:$this->tid:@$label", $old, $new);
659 private function resolveComponent($comp)
661 if ($comp instanceof MTrackComponent) {
664 if (ctype_digit($comp)) {
665 return MTrackComponent::loadById($comp);
667 return MTrackComponent::loadByName($comp);