3 * Table Definition for mtrack_ticket
5 class_exists('DB_DataObject') ? '' : require_once 'DB/DataObject.php';
7 class Pman_MTrack_DataObjects_Mtrack_ticket extends DB_DataObject
10 /* the code below is auto generated do not remove the above tag */
12 public $__table = 'mtrack_ticket'; // table name
13 public $id; // int(11) not_null primary_key auto_increment
14 public $summary; // blob(-1) blob
15 public $description; // blob(-1) blob
16 public $changelog; // blob(-1) blob
17 public $created; // int(11) not_null
18 public $updated; // int(11) not_null
19 public $owner_id; // int(11) not_null
20 public $developer_id; // int(11) not_null
22 public $priority_id; // int(11) not_null
24 public $severity_id; // int(11) not_null
25 public $classification_id; // int(11) not_null
26 public $resolution_id; // int(11) not_null
27 public $cc; // blob(65535) blob
28 public $status; // int(11) not_null
29 public $estimated; // real(22)
30 public $spent; // real(22)
31 public $x_fieldname; // blob(65535) blob
35 public $act_now; // a field to flag as ready to go.
36 // we only allow 3 of these to
37 // be active at the same time..
39 /* the code above is auto generated do not remove the tag below */
41 //checkPerm('S'/'E'/'A', $authuser) - can
42 function checkPerm($perm, $au)
44 $pr = $this->project();
45 // var_dump($pr ? $pr->name : false); var_dump($perm);
47 if (($perm == 'S') && (!$pr || ($pr->code == '*PUBLIC'))) {
53 // admins with all project rights.
54 if ($au->hasPerm('Core.Projects_All', 'S')) {
58 $pd = DB_DataObject::factory('ProjectDirectory');
59 $pd->user_id = $au->id;
60 $pd->project_id = $this->project_id;
61 $pd->whereAdd("role != ''");
72 if (!$this->project_id) {
75 $p = DB_DataObject::factory('core_project');
76 $p->get($this->project_id);
85 $p = DB_DataObject::factory('core_enum');
86 $p->get($this->status);
93 if (!$this->resolution_id) {
96 $p = DB_DataObject::factory('core_enum');
97 $p->get($this->resolution_id);
102 function classification()
104 if (!$this->classification_id) {
107 $p = DB_DataObject::factory('core_enum');
108 $p->get($this->classification_id);
115 if (!$this->developer_id) {
118 $p = DB_DataObject::factory('Person');
119 $p->get($this->developer_id);
124 function applyFilters($q, $au, $roo)
126 if (isset($q['query']['ticket_change'])) {
127 return $this->ticketChangeOptions($roo,$q, $au);
130 $tn = $this->tableName();
132 // then we only show global projects..
133 // DB_DAtaObject::DebugLevel(1);
134 $this->whereAdd(" join_project_id_id.code = '*PUBLIC' ");
135 } else if ($au->hasPerm('Core.Projects_All', 'S')) {
136 // // if we are admin?? should we show all..
139 //DB_DataObject::debugLevel(1);
140 // restrict to any projects that the user is involved in..
141 $pd = DB_DataObject::factory('ProjectDirectory');
142 $pd->person_id = $au->id;
143 $pd->whereAdd("role != ''");
146 $this->whereAdd(" join_project_id_id.code = '*PUBLIC' ");
148 // they can do what they like on all projects.
153 $ar = $pd->fetchAll('project_id');
154 $this->whereAddIn("join_project_id_id.id", $ar, 'int');
161 if ($au && isset($q['_options_for'])) {
162 $this->optionsForProject($roo, $q['_options_for']);
163 $this->jerr("somethings missing");
169 $jira_ref = "(SELECT jira_key
172 WHERE on_table='mtrack_ticket' AND on_id = {$tn}.id LIMIT 1)";
175 if (empty($q['_distinct'])) {
176 $tn = $this->tableName();
177 $this->selectAdd("$jira_ref as jira_ref ");
182 if (!empty($q['query']['search'])) {
184 $sq = $this->escape($q['query']['search']);
186 if (is_numeric($q['query']['search'])) {
187 $cond = "OR {$tn}.id = {$q['query']['search']}";
190 {$tn}.summary like '%{$sq}%'
192 {$tn}.description like '%{$sq}%'
194 $jira_ref like '%{$sq}%'
198 if (is_numeric($q['query']['search'])) {
199 // allow quick lookup by number..
206 if (!empty($q['_developer'])) {
207 switch ($q['_developer']) {
209 $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
217 if (!empty($q['query']['viewtype'])) {
219 switch ($q['query']['viewtype']) {
221 // hide "on hold" and closed..
222 $this->whereAdd("join_status_id.name NOT IN('closed', 'on hold') OR {$tn}.status = 0");
223 $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
227 $this->whereAdd("join_status_id.name = 'on hold' ");
228 $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
232 $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
233 $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
237 $this->whereAdd("join_status_id.name != 'closed' OR {$tn}.status = 0 ");
238 $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
244 $this->whereAdd("join_status_id.name ='closed'");
248 case 'active-or-recent':
250 (join_status_id.name != 'closed' OR {$tn}.status = 0)
252 (join_status_id.name = 'closed' AND join_updated.changedate > NOW() - 1 MONTH )
254 $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
258 $this->whereAdd("join_status_id.name IN ('resolved', 'needs review' )");
262 $this->whereAdd("join_status_id.name IN ('in progress' )");
266 $this->whereAdd("1=1");
270 $this->whereAdd("1=0");
278 if (!empty($q['_orderby_self_alloc '])) {
279 $auid = $au ? (int) $au->id : 0;
281 IF(developer_id = $auid , 1, 0) as is_self_alloc
283 $this->orderBy('is_self_alloc DESC');
288 if (!empty($q['query']['include_id'])) {
289 $this->whereAdd("{$tn}.id = " . ((int) $q['query']['include_id'] ), 'OR');
293 /// only show unposted..
298 function ticketChangeOptions($roo, $q, $au)
300 $et = DB_DataObject::Factory('core_enum');
302 $et->whereAddIn('etype', array(
311 // the end result of this:
316 ticketstate_[me] ????
318 ticketstate_[... everything else except close & new ...]
319 .. eg. open, reopened..
321 // these actually close the ticket...
322 // so the do not appear if the ticket is closed...
329 if (empty($q['query']['ticket_change'])) {
330 $roo->jerr("no ticket!");
333 // find out what state the ticket is in.
334 $t = DB_DataObject::Factory('mtrack_ticket');
336 if (strpos($q['query']['ticket_change'], ',') > 0) {
338 $t->status_name = 'xx';
342 $t->get($q['query']['ticket_change']);
345 if (!$et->checkPerm('S', $au)) {
346 $roo->jerr("permssion denied to query state of ticket");
351 //$this->debugLevel(1);
356 IF( id = {$t->status} , 0, 10000) +
357 IF( etype='ticketstate'
358 AND name='assigned' , 10, 0) +
359 IF( etype='ticketstate' AND
360 name !='closed' AND name != 'new' , 100, 0) +
361 IF( etype='ticketstate' , 0 , 1000) +
370 switch($t->status_name) {
373 // do not show closed..
375 ( CONCAT(etype , '.', name) != 'ticketstate.closed')
377 ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
384 ( CONCAT(etype , '.', name) = 'ticketstate.reopened')
386 core_enum.id = {$t->status}
392 ( CONCAT(etype , '.', name) != 'ticketstate.closed')
394 ( CONCAT(etype , '.', name) != 'ticketstate.new')
396 ( CONCAT(etype , '.', name) != 'ticketstate.open')
403 ( CONCAT(etype , '.', name) != 'ticketstate.closed')
405 ( CONCAT(etype , '.', name) != 'ticketstate.new')
407 ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
416 $et->orderBy("ts_order ASC");
418 $roo->jdata( $et->fetchAll(false,false,'toArray'));
425 *-- return false to let system do default sort code.
428 function applySort($au, $sortcol, $direction)
431 $direction = $direction == 'DESC' ? 'DESC' : 'ASC';
433 $tn = $this->tableName();
438 $this->orderBy("$sortcol $direction");
444 when join_status_id.display_name = 'in progress' then 1
445 when join_status_id.display_name = 'resolved' then -1
448 (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
449 join_milestone_id_id.duedate $direction ,
450 {$tn}.seqid $direction ,
451 join_milestone_id_id.name $direction
458 (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
459 join_milestone_id_id.duedate ASC ,
460 join_severity_id_id.seqid ASC,
461 join_priority_id_id.seqid ASC,
462 join_classification_id_id.seqid ASC,
463 join_updated_id.changedate DESC
469 when join_status_id.display_name = 'in progress' then 1
470 when join_status_id.display_name = 'resolved' then -1
473 join_priority_id_id.seqid $direction, {$tn}.seqid ASC
477 // this should favour in_progress, and disfavour resolved
481 when join_status_id.display_name = 'in progress' then 1
482 when join_status_id.display_name = 'resolved' then -1
485 join_priority_id_id.seqid ASC,
498 (case when join_milestone_id_id.duedate is null then 1 else 0 end),
499 join_milestone_id_id.duedate ASC ,
500 join_severity_id_id.seqid ASC,
501 join_priority_id_id.seqid ASC,
502 join_classification_id_id.seqid ASC,
503 join_updated_id.changedate DESC
517 switch ($this->status) {
524 function descriptionToHtml() /* depreciated */
526 // return MTrack_Wiki::format_to_html($this->description);
527 return htmlspecialchars($this->description);
532 function toEmailCalcAddress($changes, $person)
534 $ff = HTML_FlexyFramework::get();
537 $headers['To'] = '"'. addslashes($person->name). '" <' . $person->email .'>';
538 $headers['From'] = $ff->MTrackWeb['email_address']; // could be the user who made the change...
540 //if (count($from) > 1) {
542 // array_shift($from);
543 // foreach ($from as $email) {
544 // $rep[] = make_email($email[0], $email[1]);
547 $headers['Reply-To'] = $ff->MTrackWeb['email_address'];
553 $i = DB_DataObject::factory('Images');
554 return $i->gather($this);
557 function toEventName()
559 return $this->summary;
564 function toEmail($person, $lasttime)
566 $ff = HTML_FlexyFramework::get(); // used to work out url to show..
568 $CS = DB_DataObject::factory('mtrack_change');
569 $changes = $CS->changesSince($this,$lasttime);
570 $headers = $this->toEmailCalcAddress($changes, $person);
573 'MIME-Version' => '1.0',
574 'Content-Type' => 'text/plain; charset="UTF-8"',
575 'Content-Transfer-Encoding' => 'quoted-printable',
579 $mid = $this->id . '@' . php_uname('n');
581 $p = $this->project();
583 $subj = "[" . $p->code . ' - ' . $p->name . "] ";
585 $headers['X-mtrack-project-list'] = $p->code;
588 $headers['Subject'] = sprintf("%s#%s %s (%s %s)",
589 $subj, $this->id, $this->summary, $this->status_name, $this->classification_id_name);
591 $owner = $this->owner_id ? $this->owner_id_name : 'nobody';
594 sprintf("%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id) .
595 sprintf("#%s: %s (%s %s)\n", $this->id, $this->summary, $this->status_name, $this->classification_id_name) .
596 sprintf("Responsible: %s (%s / %s)\n", $owner, $this->priority_id_name, $this->severity_id_name) .
598 sprintf("\nDescription:\n%s\n", wordwrap($this->description,80, "\n")) .
599 //sprintf("Milestone: %s\n", join(', ', $T->getMilestones()));
600 //sprintf("Component: %s\n", join(', ', $T->getComponents()));
603 // Display changed fields grouped by the person that last changed them
604 //$who_changed = array();
605 //foreach ($field_changers as $field => $who) {
606 // $who_changed[$who][] = $field;
609 foreach ($changes as $change) {
611 $body .= "\n{$change->changedate}: Change by {$change->person_id_name}\n";
612 $body .= str_repeat('-', 80) . "\n";
613 if (!empty($change->reason)) {
614 $body.= wordwrap($change->reason, 80, "\n") ."\n\n";
616 $ar = $change->cachedAudit();
618 foreach($ar as $audit) {
620 switch($audit->field()) {
622 continue 2; //??? ignore?
635 if ($audit->oldvalue == $audit->value) {
636 continue 2; // no change?!?!?
638 $oldvalue = $audit->oldvalue($change);
639 $value = $audit->value($change);
640 $field = $audit->field();
642 $field = preg_replace('/_id$/', '', $field);
643 $field = str_replace('/_/', ' ', $field);
644 $lb = strpos($oldvalue,"\n") > -1 || strpos($value,"\n") > -1 ? "\n" : '';
646 if (!strlen($oldvalue)) {
647 $body .= "Set {$field} to: {$lb}{$value}\n";
651 if (!strlen($value)) {
652 $body .= "Removed {$field} - was: {$lb}{$oldvalue}\n";
655 $body .= "Changed {$field} from : {$lb}{$oldvalue} {$lb}-> {$lb}{$value}\n";
661 $body .= sprintf("\n\n%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id);
665 'headers' => $headers
672 function toEventString()
674 return '#'.$this->id . ' ' . $this->summary;
678 var $_mtrack_change = false;
680 function beforeUpdate($old, $req, $roo)
683 if (!empty($req['reason'])) {
684 $ch = DB_DataObject::factory('mtrack_change');
685 $ch->beginChange($this, $req['reason']);
686 $this->_mtrack_change= $ch;
688 // used by gitlive close ticket...
689 if (!empty($req['status_name']) && empty($req['status'])) {
690 $n = DB_DataObject::Factory('core_enum');
691 $n->name = $req['status_name'];
692 $n->whereAddIn('etype', array('ticketstate','resolution'),'string');
693 if (!$n->find(true)) {
694 $roo->jerr("invalid status name");
696 $this->status = $n->id;
699 if ($old->status != $this->status) {
700 // status has been changed. - check to see if it's really a resolution.
701 $n = DB_DataObject::Factory('core_enum');
702 if (!$this->status || !$n->get($this->status)) {
703 $roo->jerr("status set invalid");
705 if ($n->etype != 'ticketstate') {
706 if ($n->etype != 'resolution') {
707 $roo->jerr("status set invalid");
709 $this->resolution_id = $n->id;
710 $n = DB_DataObject::Factory('core_enum');
711 $n->etype = 'ticketstate';
712 $n->get('name', 'closed'); // no error checking!?!
713 $this->status = $n->id;
721 if (isset($req['_reorder'])) {
722 $old->updateSequence($req['_reorder']);
723 $roo->jok("UPDATED");
726 if (isset($req['_before']) || isset($req['_after'])) {
727 $req['_after'] = isset($req['_after']) ? $req['_after'] : 0;
728 $req['_before'] = isset($req['_before']) ? $req['_before'] : 0;
729 $old->updateSequence($req['_before'],$req['_after']);
732 if ($req['_before']) {
733 $before = DB_DataObject::factory('mtrack_ticket');
734 $before->get($req['_before']);
735 $this->seqid = $before->seqid;
736 } else if ($req['_after']) {
737 $after = DB_DataObject::factory('mtrack_ticket');
738 $after->get($req['_after']);
739 $this->seqid = $after->seqid+1;
751 function beforeInsert($req, $roo)
754 if (isset($req['_bulk_update'])) {
755 $this->bulkUpdate($req,$roo);
759 if (!$this->status) {
760 $n = DB_DataObject::Factory('core_enum');
761 $n->etype = 'ticketstate';
762 $n->get('name', 'new'); // no error checking!?!
763 $this->status = $n->id;
768 function bulkUpdate($req, $roo)
770 if (empty($req['_bulk_update'])) {
771 $roo->jerr("invalid tickets");
774 $t = DB_DAtaObject::Factory('mtrack_ticket');
776 $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
777 $all = $t->fetchAll();
779 foreach($req as $k=>$v) {
785 foreach($all as $t) {
788 $t->beforeUpdate($old, $cg, $roo);
790 $t->onUpdate($old,$req, $roo);
793 $t = DB_DAtaObject::Factory('mtrack_ticket');
795 $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
796 $roo->jok($t->fetchAll(false,false,'toArray'));
807 function onUpdate($old,$req, $roo)
809 //print_r($old); print_r($this);
812 $ch = $this->_mtrack_change;
814 $ch = DB_DataObject::factory('mtrack_change');
815 $ch->beginChange($this, "Changed");
816 $this->_mtrack_change = $ch;
819 $n = $ch->add($this, $old);
820 if (!$n && !$this->_mtrack_change) {
821 //die("deleting change");
822 $ch->delete(); // do not record the change..
826 $this->updated = $ch->id;
837 $ch = DB_DataObject::factory('mtrack_change');
838 $ch->get($this->updated ? $this->updated : $this->created);
844 function toRooSingleArray($au, $req)
847 return $this->toArray();
850 $p = $this->factory('core_enum');
851 $p->etype='priority';
852 $p->get('name', 'normal');
854 $s = $this->factory('core_enum');
855 $s->etype='severity';
856 $s->get('name', 'normal');
858 $c = $this->factory('core_enum');
859 $c->etype='classification';
860 $c->get('name', 'enhancement');
862 $this->severity_id = $s->id;
863 $this->classification_id = $c->id;
864 $this->priority_id = $p->id;
865 $ret = $this->toArray();
866 $ret = array_merge($ret, $p->toArray('priority_id_%s'));
867 $ret = array_merge($ret, $c->toArray('classification_id_%s'));
868 $ret = array_merge($ret, $s->toArray('severity_id_%s'));
874 // http://roojs.com/admin.php/Roo/Mtrack_ticket?_post=1&id=733&_reorder=0
875 function updateSequence($tbefore, $tafter = 0)
877 // fix empty sequences?
879 $tn = $this->tableName();
880 $t = DB_DataObject::factory('mtrack_ticket');
883 //$t->project_id = $this->project_id;
886 // we still apply to closed tickets????
887 $t->whereAdd("join_status_id.name != 'closed'");
888 $t->applySort(false, 'old_style', false);
890 $old_seqid = $this->seqid;
893 // add a seq id to everything.
894 $t->selectAdd("{$tn}.id as id");
895 $ids = $t->fetchAll('id');
897 $t_all = DB_DataObject::factory('mtrack_ticket');
898 $t_all->selectAdd('max(seqid) as seqid');
901 $start = empty($t_all->seqid) ? 1 : $t_all->seqid;
903 foreach($ids as $i => $id) {
904 $tt->query("UPDATE {$tn} SET seqid = " . ($start + $i) ." WHERE id = {$id}");
906 $t = DB_DataObject::Factory('mtrack_ticket');
908 $old_seqid = $t->seqid;
918 $before = DB_DataObject::factory('mtrack_ticket');
919 $before->get($tbefore);
920 $before_seq_id = $before->seqid;
924 $after = DB_DataObject::factory('mtrack_ticket');
925 $after->get($tafter);
926 $before_seq_id = $after->seqid+1;
929 $t = DB_DataObject::factory('mtrack_ticket');
932 /// move everything down.
933 $t->query("UPDATE {$tn} SET
936 seqid > {$before_seq_id}
941 // puts this one in the gap..
942 $t->query("UPDATE {$tn} SET seqid = ". ( $before_seq_id + 1) . " WHERE id = {$this->id}");
944 // now there is a gap where this used to be..
947 $t->query("UPDATE {$tn} SET
955 function toRooArray($q) {
956 if (empty($q['_future_schedule'])) {
957 return $this->toArray();
959 if (!$this->developer_id) {
962 $add = $this->toCalEvent();
963 if (isset($q['filter'])) {
964 foreach($q['filter'] as $k=>$v) {
968 if (!isset($add[$k])) {
969 $roo->jerr('invalid filter ' .$k);
971 if ($v != $add[$k]) {
982 // only run this once..
983 function toCalEvent()
985 //secho '<PRE>';print_r($this);exit;
986 static $cdata = array();
989 if (empty($cdata[$this->developer_id])) {
990 $start = $this->availStart(date('Y-m-d H:i:s'));
992 $start = $this->availStart($cdata[$this->developer_id]);
995 $end = $this->availEnd($start, $this->estimated ? $this->estimated : 1);
996 $cdata[$this->developer_id]= $end;
998 $ret = array_merge($this->toArray(), array(
999 'start_dt' => $start,
1002 'title' => '#' . $this->id . ' ' .$this->developer_id_name . ' : ' . $this->project_id_name,
1003 'description' => $this->summary,
1011 function availStart($dt)
1013 //give a date time.. see if we should schedule it today?
1014 $st = strtotime($dt);
1016 if (date('H', $st) < 11) {
1017 $st = strtotime(date('Y-m-d 10:00:00',$st));
1019 if (date('H', $st) > 17) {
1020 if(date('N',$st) > 4) {
1021 // it's friday... (5) +3 , sat (6) +2 sun(7) +1
1022 $days = 8 - date('N',$st);
1023 $st = strtotime(date('Y-m-d',$st).' + ' . $days . ' DAY');
1026 $st = strtotime(date('Y-m-d',$st).' + 1 DAY');
1028 $st = strtotime(date('Y-m-d 10:00:00',$st));
1029 return date('Y-m-d H:i:s', $st);
1031 // otherwise this time is OK...
1034 function availEnd($start, $hours) {
1037 for ($i = 0; $i < ceil($hours ); $i++) {
1038 $et = $this->availStart(date('Y-m-d H:i:s', strtotime($et . ' + 1 HOUR')));
1044 function optionsForProject($roo,$pid)
1047 $m = DB_DataObject::factory('mtrack_milestone');
1048 $m->project_id = $pid;
1050 $m->selectAdd('id,name as name, name as display_name');
1051 $m->orderBy('startdate ASC');
1052 $ret['milestone'] = $m->fetchAll(false,false,'toArray');
1054 $mm = DB_DataObject::factory('core_enum');
1055 $mm->orderBy('seqid ASC');
1059 $m->etype = 'priority';
1060 $ret['priority'] = $m->fetchAll(false,false,'toArray');
1063 $m->etype = 'severity';
1064 $ret['severity'] = $m->fetchAll(false,false,'toArray');
1067 $m->etype = 'classification';
1068 $ret['classification'] = $m->fetchAll(false,false,'toArray');
1072 $m = DB_DataObject::factory('core_person');
1073 $m->company_id = $roo->company->id;
1075 $m->selectAdd("id,name as name, concat(name ,' <', email , '>') as display_name");
1077 $m->applyFilters(array(
1079 'project_id' => $pid,
1080 'role' => 'DEVELOPER'
1082 ),$roo->authUser, $roo);
1084 $m->orderBy('name ASC');
1085 $ret['developer'] = $m->fetchAll(false,false,'toArray');
1086 $ret['authuser_id'] = "".$roo->authUser->id; //string...