fetchAll(PDO::FETCH_COLUMN, 0); if (count($ids) == 1) { $cache[$id] = $ids[0]; } else { return null; } } return new MTrackIssue($cache[$id]); } static function registerListener(IMTrackIssueListener $l) // used by CustomFields { self::$_listeners[] = $l; } static function index_issue($object) { list($ignore, $ident) = explode(':', $object, 2); $i = MTrackIssue::loadById($ident); if (!$i) return; echo "Ticket #$i->nsident\n"; $CS = MTrackChangeset::get($i->updated); $CSC = MTrackChangeset::get($i->created); $kw = join(' ', array_values($i->getKeywords())); $idx = array( 'summary' => $i->summary, 'description' => $i->description, 'changelog' => $i->changelog, 'keyword' => $kw, 'stored:date' => $CS->when, 'who' => $CS->who, 'creator' => $CSC->who, 'stored:created' => $CSC->when, 'owner' => $i->owner ); $i->augmentIndexerFields($idx); MTrackSearchDB::add("ticket:$i->tid", $idx, true); foreach (MTrackDB::q('select value, changedate, who from change_audit left join changes using (cid) where fieldname = ?', "ticket:$ident:@comment") as $row) { list($text, $when, $who) = $row; $start = time(); $id = sha1($text); $elapsed = time() - $start; if ($elapsed > 4) { echo " - comment $who $when took $elapsed to hash\n"; } $start = time(); if (strlen($text) > 8192) { // A huge paste into a ticket $text = substr($text, 0, 8192); } MTrackSearchDB::add("ticket:$ident:comment:$id", array( 'description' => $text, 'stored:date' => $when, 'who' => $who, ), true); $elapsed = time() - $start; if ($elapsed > 4) { echo " - comment $who $when took $elapsed to index\n"; } } } //methods.. function __construct($tid = null) { if ($tid === null) { $this->components = array(); $this->origcomponents = array(); $this->milestones = array(); $this->origmilestones = array(); $this->keywords = array(); $this->origkeywords = array(); $this->status = 'new'; foreach (array('classification', 'severity', 'priority') as $f) { $this->$f = MTrackConfig::get('ticket', "default.$f"); } } else { $data = MTrackDB::q('select * from tickets where tid = ?', $tid)->fetchAll(); $row = null; if (isset($data[0])) { $row = $data[0]; } if (!is_array($row)) { throw new Exception("no such issue $tid"); } foreach ($row as $k => $v) { $this->$k = $v; } } } function applyPOSTData($data) { foreach (self::$_listeners as $l) { $l->applyPOSTData($this, $data); } } function augmentFormFields(&$FIELDSET) { foreach (self::$_listeners as $l) { $l->augmentFormFields($this, $FIELDSET); } } function augmentIndexerFields(&$idx) { foreach (self::$_listeners as $l) { $l->augmentIndexerFields($this, $idx); } } function augmentSaveParams(&$params) { foreach (self::$_listeners as $l) { $l->augmentSaveParams($this, $params); } } function checkVeto() { $args = func_get_args(); $method = array_shift($args); $veto = array(); foreach (self::$_listeners as $l) { $v = call_user_func_array(array($l, $method), $args); if ($v !== true) { $veto[] = $v; } } if (count($veto)) { $reasons = array(); foreach ($veto as $r) { if (is_array($r)) { foreach ($r as $m) { $reasons[] = $m; } } else { $reasons[] = $r; } } require_once 'Exception/Veto.php'; throw new MTrackVetoException($reasons); } } function save(MTrackChangeset $CS) { $db = MTrackDB::get(); $reindex = false; if ($this->tid === null) { $this->created = $CS->cid; $oldrow = array(); $reindex = true; } else { list($oldrow) = MTrackDB::q('select * from tickets where tid = ?', $this->tid)->fetchAll(); } $this->checkVeto('vetoSave', $this, $oldrow); $this->updated = $CS->cid; $params = array( 'summary' => $this->summary, 'description' => $this->description, 'created' => $this->created, 'updated' => $this->updated, 'owner' => $this->owner, 'changelog' => $this->changelog, 'priority' => $this->priority, 'severity' => $this->severity, 'classification' => $this->classification, 'resolution' => $this->resolution, 'status' => $this->status, 'estimated' => (float)$this->estimated, 'spent' => (float)$this->spent, 'nsident' => $this->nsident, 'cc' => $this->cc, ); $this->augmentSaveParams($params); if ($this->tid === null) { $sql = 'insert into tickets '; $keys = array(); $values = array(); require_once 'UUID.php'; $new_tid = new OmniTI_Util_UUID; $new_tid = $new_tid->toRFC4122String(false); $keys[] = "tid"; $values[] = "'$new_tid'"; foreach ($params as $key => $value) { $keys[] = $key; $values[] = ":$key"; } $sql .= "(" . join(', ', $keys) . ") values (" . join(', ', $values) . ")"; } else { $sql = 'update tickets set '; $values = array(); foreach ($params as $key => $value) { $values[] = "$key = :$key"; } $sql .= join(', ', $values) . " where tid = :tid"; $params['tid'] = $this->tid; } $q = $db->prepare($sql); $q->execute($params); if ($this->tid === null) { $this->tid = $new_tid; $created = true; } else { $created = false; } foreach ($params as $key => $value) { if ($key == 'created' || $key == 'updated' || $key == 'tid') { continue; } if ($key == 'changelog' || $key == 'description' || $key == 'summary') { if (!isset($oldrow[$key]) || $oldrow[$key] != $value) { $reindex = true; } } if (!isset($oldrow[$key])) { $oldrow[$key] = null; } $CS->add("ticket:$this->tid:$key", $oldrow[$key], $value); } $this->compute_diff($CS, 'components', 'ticket_components', 'compid', $this->components, $this->origcomponents); $this->compute_diff($CS, 'keywords', 'ticket_keywords', 'kid', $this->keywords, $this->origkeywords); $this->compute_diff($CS, 'milestones', 'ticket_milestones', 'mid', $this->milestones, $this->origmilestones); foreach ($this->comments_to_add as $text) { $CS->add("ticket:$this->tid:@comment", null, $text); } foreach ($this->effort as $effort) { MTrackDB::q('insert into effort (tid, cid, expended, remaining) values (?, ?, ?, ?)', $this->tid, $CS->cid, $effort[0], $effort[1]); } $this->effort = array(); } function getComponents() { if ($this->components !== null) { return $this->components ; } $q = MTrackDB::q(' select tc.compid, name from ticket_components tc left join components using (compid) where tid = ?', $this->tid); $this->origcomponents = array(); foreach ($q->fetchAll() as $row) { $this->origcomponents[$row[0]] = $row[1]; } $this->components = $this->origcomponents; return $this->components; } function assocComponent($comp) { $comp = $this->resolveComponent($comp); $this->getComponents(); $this->checkVeto('vetoComponent', $this, $comp, true); $this->components[$comp->compid] = $comp->name; } function dissocComponent($comp) { $comp = $this->resolveComponent($comp); $this->getComponents(); $this->checkVeto('vetoComponent', $this, $comp, false); unset($this->components[$comp->compid]); } function getMilestones() { if ($this->milestones === null) { $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(); $this->origmilestones = array(); foreach ($comps as $row) { $this->origmilestones[$row[0]] = $row[1]; } $this->milestones = $this->origmilestones; } return $this->milestones; } function assocMilestone($M) { $ms = $this->resolveMilestone($M); if ($ms === null) { throw new Exception("unable to resolve milestone $M"); } $this->getMilestones(); $this->checkVeto('vetoMilestone', $this, $ms, true); $this->milestones[$ms->mid] = $ms->name; } function dissocMilestone($M) { $ms = $this->resolveMilestone($M); if ($ms === null) { throw new Exception("unable to resolve milestone $M"); } $this->getMilestones(); $this->checkVeto('vetoMilestone', $this, $ms, false); unset($this->milestones[$ms->mid]); } function addComment($comment) { $comment = trim($comment); if (strlen($comment)) { $this->checkVeto('vetoComment', $this, $comment); $this->comments_to_add[] = $comment; } } function assocKeyword($kw) { $kw = $this->resolveKeyword($kw); $this->getKeywords(); $this->checkVeto('vetoKeyword', $this, $kw, true); $this->keywords[$kw->kid] = $kw->keyword; } function dissocKeyword($kw) { $kw = $this->resolveKeyword($kw); $this->getKeywords(); $this->checkVeto('vetoKeyword', $this, $kw, false); unset($this->keywords[$kw->kid]); } function getKeywords() { if ($this->keywords === null) { $comps = MTrackDB::q('select tc.kid, keyword from ticket_keywords tc left join keywords using (kid) where tid = ?', $this->tid)->fetchAll(); $this->origkeywords = array(); foreach ($comps as $row) { $this->origkeywords[$row[0]] = $row[1]; } $this->keywords = $this->origkeywords; } return $this->keywords; } function addEffort($amount, $revised = null) { $diff = null; if ($revised !== null) { $diff = $revised - $this->estimated; $this->estimated = $revised; } $this->effort[] = array($amount, $diff); $this->spent += $amount; } function close() { $this->status = 'closed'; $this->addEffort(0, 0); } function isOpen() { switch ($this->status) { case 'closed': return false; default: return true; } } function reOpen() { $this->status = 'reopened'; $this->resolution = null; } function toArray() { $ret = get_object_vars($this); // echo '
'; print_r($ret);exit;
        return $ret;
    }
  
  
  /// rendering tricks
  
  
    function updateWho($link)
    {
        return  $link->username(
                $this->updated ? 
                    MTrackChangeset::get($this->updated)->who :
                    MTrackAuth::whoami()
        );
        
    }
    function updateWhen($link) 
    {
        return  $link->date( $this->updated ? 
                MTrackChangeset::get($this->updated)->when :
                MTrackDB::unixtime(time()) 
        );
    }
    function createdWho($link)
    {
        return  $link->username(
                $this->created ? 
                    MTrackChangeset::get($this->created)->who :
                    MTrackAuth::whoami()
        );
        
    }
    function createdWhen($link) 
    {
        return  $link->date( $this->created ? 
                MTrackChangeset::get($this->created)->when :
                MTrackDB::unixtime(time()) 
        );
    }
  
    function keywordsToHtml() 
    {
        $value = array();
        foreach ($this->getKeywords() as $kw) {
            $value[] = mtrack_keyword($kw);
        }
        return  join(' ', $value);
    }
    

    
    function componentsToHtml() 
    {
        $res = array();
        $value = join(',',array_keys($this->getComponents()));
        
        if (!strlen($value)) {
            return '';
        }
        $q = MTrackDB::q(
              "select name, deleted from components where compid in ($value)");
        foreach ($q->fetchAll() as $row) {
            
            $c = ($row['deleted'] ? '' : '') .
                htmlentities($row['name'], ENT_QUOTES, 'utf-8') . 
                ($row['deleted'] ? '' : '');
            $res[] = $c;
        }
        
        return join(", ", $res);
       
    }
    
    function milestonesToHtml($msurl) 
    {
        if (empty($this->milestone_url)) {
            die("requires issue->milestone_url to be set.");
        }
        $res = array();
        $value = join(',',array_keys($this->getMilestones()));
        
        if (!strlen($value)) {
            return '';
        }
        foreach (MTrackDB::q(
          "select name, completed, deleted from milestones where mid in ($value)")
          ->fetchAll() as $row) {
              
            $row['deleted'] =   strlen($row['completed']) ? 1 : 0;
                
            $c = "milestone_url}" . urlencode($row['name']) . '">' .
                htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
                "";
            $res[] = $c;
        }
        return join(", ", $res);
    }
    function descriptionToHtml()
    {
        return MTrack_Wiki::format_to_html($this->description);
    }
   /* function attachmentsToHtml()
    {
        require_once 'Attachment.php';
        return MTrackAttachment::renderList("ticket:{$this->tid}");
    }
    function attachmentsDeleteToHtml() {
        require_once 'Attachment.php';
        return MTrackAttachment::renderDeleteList("ticket:{$this->tid}");
    }
 */
    function toIdString() {
        return "ticket:{$this->tid}";
            
    }
    
    // watcher related.
    function watcherButton()
    {
        return MTrackWatch::renderWatchUI('ticket', $this->tid);
    }
    
    function watchType() 
    {
        return 'ticket';
    }
    function watchId()
    {
        return $this->tid;
    }
    
    function watchers()
    {
        return MTrackWatch::objectWatchers($this);
    }
    
    
    private function resolveMilestone($ms)
    {
        if ($ms instanceof MTrack_Milestone) {
          return $ms;
        }
        if (ctype_digit($ms)) {
          return MTrack_Milestone::loadById($ms);
        }
        return MTrack_Milestone::loadByName($ms);
    }

  private function resolveKeyword($kw)
  {
    if ($kw instanceof MTrackKeyword) {
      return $kw;
    }
    $k = MTrackKeyword::loadByWord($kw);
    if ($k === null) {
      if (ctype_digit($kw)) {
        return MTrackKeyword::loadById($kw);
      }
      throw new Exception("unknown keyword $kw");
    }
    return $k;
  }
  private function compute_diff(MTrackChangeset $CS, $label,
        $tablename, $keyname, $current, $orig) {
    if (!is_array($current)) {
      $current = array();
    }
    if (!is_array($orig)) {
      $orig = array();
    }
    $added = array_keys(array_diff_key($current, $orig));
    $removed = array_keys(array_diff_key($orig, $current));

    $db = MTrackDB::get();
    $ADD = $db->prepare(
      "insert into $tablename (tid, $keyname) values (?, ?)");
    $DEL = $db->prepare(
      "delete from $tablename where tid = ? AND $keyname = ?");
    foreach ($added as $key) {
      $ADD->execute(array($this->tid, $key));
    }
    foreach ($removed as $key) {
      $DEL->execute(array($this->tid, $key));
    }
    if (count($added) + count($removed)) {
      $old = join(',', array_keys($orig));
      $new = join(',', array_keys($current));
      $CS->add(
        "ticket:$this->tid:@$label", $old, $new);
    }
  }
  private function resolveComponent($comp)
  {
    if ($comp instanceof MTrackComponent) {
      return $comp;
    }
    if (ctype_digit($comp)) {
      return MTrackComponent::loadById($comp);
    }
    return MTrackComponent::loadByName($comp);
  }
}