81e6df4f3aab12bafe233520502e3d075757c3a7
[Pman.MTrack] / DataObjects / Mtrack_ticket.php
1 <?php
2 /**
3  * Table Definition for mtrack_ticket
4  */
5 class_exists('DB_DataObject') ? '' : require_once 'DB/DataObject.php';
6
7 class Pman_MTrack_DataObjects_Mtrack_ticket extends DB_DataObject 
8 {
9     ###START_AUTOCODE
10     /* the code below is auto generated do not remove the above tag */
11
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
21
22     public $priority_id;                     // int(11)  not_null
23     
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
32     public $project_id;
33     public $milestone_id;
34     
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..
38     
39     /* the code above is auto generated do not remove the tag below */
40     ###END_AUTOCODE
41     //checkPerm('S'/'E'/'A', $authuser) - can
42     function checkPerm($perm, $au)
43     {
44         $pr = $this->project();
45         // var_dump($pr ? $pr->name : false); var_dump($perm);
46         
47         if (($perm == 'S')  && (!$pr || ($pr->code == '*PUBLIC'))) {
48             return true;
49         }
50         if (!$au) {
51             return false;
52         }
53         // admins with all project rights.
54         if ($au->hasPerm('Core.Projects_All', 'S')) {
55             return true;
56         }
57          
58         $pd = DB_DataObject::factory('ProjectDirectory');
59         $pd->user_id = $au->id;
60         $pd->project_id = $this->project_id;
61         $pd->whereAdd("role != ''");
62         // not        
63         if ($pd->count()) {
64             return true;
65         }
66         return false;
67         
68     }
69     
70     function project()
71     {
72         if (!$this->project_id) {
73             return false;
74         }
75         $p = DB_DataObject::factory('Projects');
76         $p->get($this->project_id);
77         return $p;
78          
79     }
80     function status()
81     {
82         if (!$this->status) {
83             return false;
84         }
85         $p = DB_DataObject::factory('core_enum');
86         $p->get($this->status);
87         return $p;
88          
89     }
90     
91     function resolution()
92     {
93         if (!$this->resolution_id) {
94             return false;
95         }
96         $p = DB_DataObject::factory('core_enum');
97         $p->get($this->resolution_id);
98         return $p;
99          
100     }
101     
102      function classification()
103     {
104         if (!$this->classification_id) {
105             return false;
106         }
107         $p = DB_DataObject::factory('core_enum');
108         $p->get($this->classification_id);
109         return $p;
110          
111     }
112     
113     function developer()
114     {
115         if (!$this->developer_id) {
116             return false;
117         }
118         $p = DB_DataObject::factory('Person');
119         $p->get($this->developer_id);
120         return $p;
121         
122     }
123     
124     function applyFilters($q, $au, $roo)
125     {
126         if (isset($q['query']['ticket_change'])) {
127             return $this->ticketChangeOptions($roo,$q, $au);
128         }
129         
130         $tn = $this->tableName();
131         if (!$au) {
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..
137                
138         } else {
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 != ''");
144             
145             if (!$pd->count()) {
146                 $this->whereAdd(" join_project_id_id.code = '*PUBLIC' ");
147             
148                 // they can do what they like on all projects.
149                 // no op..
150             } else {
151                 
152                 
153                 $ar = $pd->fetchAll('project_id');
154                 $this->whereAddIn("join_project_id_id.id", $ar, 'int');
155                 
156             }
157              
158             
159         }
160         
161         if ($au && isset($q['_options_for'])) {
162             $this->optionsForProject($q['_options_for']);
163             $this->jerr("somethings missing");
164             
165         }
166         
167         
168         
169         $jira_ref =  "(SELECT jira_key
170                     FROM
171                         mtrack_jira_object
172                     WHERE on_table='mtrack_ticket' AND on_id = {$tn}.id LIMIT 1)";
173           
174         
175         if (empty($q['_distinct'])) {
176             $tn = $this->tableName();
177             $this->selectAdd("$jira_ref as jira_ref ");
178         }    
179           
180           
181         //var_dump($q);exit;
182         if (!empty($q['query']['search'])) {
183             
184             $sq = $this->escape($q['query']['search']);
185             $cond = '';
186             if (is_numeric($q['query']['search'])) {
187                 $cond = "OR {$tn}.id = {$q['query']['search']}";
188             }
189             $this->whereAdd(" 
190                  {$tn}.summary like '%{$sq}%'
191                  OR
192                  {$tn}.description like '%{$sq}%'
193                  OR
194                  $jira_ref like '%{$sq}%'
195                 $cond
196             ");
197             
198             if (is_numeric($q['query']['search'])) {
199                 // allow quick lookup by number..
200                 
201                 return;
202             }
203             
204         }
205             
206         
207         
208         if (!empty($q['query']['viewtype'])) {
209              
210             switch ($q['query']['viewtype']) {
211                 case 'active':
212                     // hide "on hold" and closed..
213                     $this->whereAdd("join_status_id.name NOT IN('closed', 'on hold') OR  {$tn}.status  = 0");
214                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
215                     break;
216                 
217                 case 'on-hold':
218                     $this->whereAdd("join_status_id.name = 'on hold' ");
219                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
220                     break;
221                 
222                 case 'my':
223                     $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
224                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
225                     
226                 case 'me':
227                 case 'me-all':
228                     $this->whereAdd("join_status_id.name != 'closed' OR  {$tn}.status  = 0 ");
229                     $this->whereAdd("mtrack_ticket.developer_id = {$au->id} ");
230                     
231                     break;
232                 
233                   
234                 case 'closed':
235                     $this->whereAdd("join_status_id.name ='closed'");
236                     
237                     break;
238                  
239                 case 'active-or-recent':
240                     $this->whereAdd("
241                             (join_status_id.name != 'closed' OR  {$tn}.status  = 0)
242                             OR
243                             (join_status_id.name = 'closed' AND join_updated.changedate > NOW() - 1 MONTH  )
244                             ");
245                     $this->whereAdd("{$tn}.milestone_id = 0 OR join_milestone_id_id.on_hold = 0");
246                     break;
247                 
248                 case 'pending':
249                     $this->whereAdd("join_status_id.name IN ('resolved', 'needs review' )");
250                     break;
251                 
252                 case 'in-progress':
253                     $this->whereAdd("join_status_id.name IN ('in progress' )");
254                     break;
255                     
256                 case 'all':
257                     $this->whereAdd("1=1");
258                     break;
259                 
260                 default :
261                     $this->whereAdd("1=0");
262                 
263             }
264             
265             
266             
267         }
268         
269         if (!empty($q['_orderby_self_alloc '])) {
270             $auid = $au ? (int) $au->id : 0;
271             $this->selectAdd(" 
272                   IF(developer_id = $auid , 1, 0) as is_self_alloc         
273                              ");
274             $this->orderBy('is_self_alloc DESC');
275             
276             
277         }
278         
279         if (!empty($q['query']['include_id'])) {
280             $this->whereAdd("{$tn}.id = " . ((int) $q['query']['include_id'] ), 'OR');
281         }
282         
283         
284         /// only show unposted..
285         
286     }
287     
288     
289     function ticketChangeOptions($roo, $q, $au)
290     {
291             $et = DB_DataObject::Factory('core_enum');
292             $et->active  = 1;
293             $et->whereAddIn('etype', array(
294                     'ticketstate',
295                     'resolution',
296                     //'classification',
297                     //'severity',
298                     //'priority'
299             ),'string') ;
300            
301             
302             // the end result of this:
303             /*
304                
305                
306                ticketstate_[same]
307                ticketstate_[me] ????
308                
309                ticketstate_[... everything else except close & new ...]
310                   .. eg. open, reopened..
311                
312                // these actually close the ticket...
313                // so the do not appear if the ticket is closed...
314                resolution:
315                     
316              
317              
318             */
319             
320             if (empty($q['query']['ticket_change'])) {
321                 $roo->jerr("no ticket!");
322             }
323             
324             // find out what state the ticket is in.
325             $t = DB_DataObject::Factory('mtrack_ticket');
326             
327             if (strpos($q['query']['ticket_change'], ',')  > 0) {
328                 $t->status = 0;
329                 $t->status_name = 'xx';
330                 
331             } else {
332                 $t->autoJoin();
333                 $t->get($q['query']['ticket_change']);
334             
335             }
336             if (!$et->checkPerm('S', $au)) {
337                 $roo->jerr("permssion denied to query state of ticket");
338             }
339             
340
341             
342             //$this->debugLevel(1);
343             
344             
345             
346             $et->selectAdd("
347                 IF( id = {$t->status} , 0, 10000) +
348                 IF( etype='ticketstate'
349                     AND name='assigned' , 10, 0) +
350                 IF( etype='ticketstate' AND
351                         name !='closed'  AND  name != 'new' , 100, 0) +
352                 IF( etype='ticketstate' , 0 , 1000) +
353
354                 seqid
355                 AS
356                 ts_order
357                              
358             ");
359             
360             // hide stuff...
361             switch($t->status_name) {
362                 
363                 case 'new':
364                     // do not show closed..
365                     $et->whereAdd("
366                         ( CONCAT(etype , '.', name) != 'ticketstate.closed')
367                         AND
368                          ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
369                     ");
370                     break;
371                   
372                 case 'closed':
373                     // 
374                     $et->whereAdd("
375                                     ( CONCAT(etype , '.', name) = 'ticketstate.reopened')
376                                     OR
377                                     core_enum.id = {$t->status}
378                     ");
379                     break;
380                 
381                 case 'reopened':
382                     $et->whereAdd("
383                              ( CONCAT(etype , '.', name) != 'ticketstate.closed')
384                              AND
385                             ( CONCAT(etype , '.', name) != 'ticketstate.new')
386                              AND
387                            ( CONCAT(etype , '.', name) != 'ticketstate.open')
388                             ");
389                     break;
390
391                 case 'open':
392                 default:
393                     $et->whereAdd("
394                            ( CONCAT(etype , '.', name) != 'ticketstate.closed')
395                              AND
396                             ( CONCAT(etype , '.', name) != 'ticketstate.new')
397                              AND
398                            ( CONCAT(etype , '.', name) != 'ticketstate.reopened')
399                             ");
400                     break;
401                 
402                 
403                    
404             }
405             
406             
407             $et->orderBy("ts_order ASC");
408             
409             $roo->jdata( $et->fetchAll(false,false,'toArray'));
410             exit;
411             
412     }
413     
414     
415     /**
416      *-- return false to let system do default sort code.
417      **/
418     
419     function applySort($au, $sortcol, $direction)
420     {
421         // fixme smarter?
422         $direction = $direction == 'DESC' ? 'DESC' : 'ASC';
423         
424         $tn = $this->tableName();
425         switch( $sortcol ) {
426             
427             case 'id':
428             //..... others..
429                 $this->orderBy("$sortcol $direction");
430                 return;
431             
432             case 'milestone_id':
433                    $this->orderBy("
434                         case
435                             when join_status_id.display_name = 'in progress' then 1
436                             when join_status_id.display_name = 'resolved' then -1
437                             else 0
438                         end DESC,
439                        (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
440                             join_milestone_id_id.duedate $direction ,
441                               {$tn}.seqid $direction ,
442                               join_milestone_id_id.name $direction
443                        ");
444                   
445                 return;
446                 
447             case 'old_style':
448                 $this->orderBy('
449                        (case when join_milestone_id_id.duedate is null then 1 else 0 end) ASC,
450                             join_milestone_id_id.duedate ASC ,
451                             join_severity_id_id.seqid ASC,
452                             join_priority_id_id.seqid ASC,
453                             join_classification_id_id.seqid ASC,
454                             join_updated_id.changedate  DESC
455                        ');
456                 return;
457             case 'priority_id':
458                 $this->orderBy("
459                         case
460                             when join_status_id.display_name = 'in progress' then 1
461                             when join_status_id.display_name = 'resolved' then -1
462                             else 0
463                         end DESC,
464                         join_priority_id_id.seqid $direction, {$tn}.seqid ASC
465                     ");
466                 break;
467             default:
468                 // this should favour in_progress, and disfavour resolved
469                 
470                 $this->orderBy("
471                         case
472                             when join_status_id.display_name = 'in progress' then 1
473                             when join_status_id.display_name = 'resolved' then -1
474                             else 0
475                         end DESC,
476                         join_priority_id_id.seqid ASC,
477                         {$tn}.seqid ASC   ");
478                         
479                
480                             
481                 
482                 return;
483         }
484         
485     }
486     
487     /*
488      
489      (case when join_milestone_id_id.duedate is null then 1 else 0 end),
490                             join_milestone_id_id.duedate ASC ,
491                             join_severity_id_id.seqid ASC,
492                             join_priority_id_id.seqid ASC,
493                             join_classification_id_id.seqid ASC,
494                             join_updated_id.changedate  DESC
495     */
496     
497     
498     
499     
500     
501     
502     
503     
504     
505     
506     function isOpen()
507     {
508         switch ($this->status) {
509           case 'closed':
510             return false;
511           default:
512             return true;
513         }
514     }
515     function descriptionToHtml() /* depreciated */
516     {
517        // return MTrack_Wiki::format_to_html($this->description);
518         return htmlspecialchars($this->description);
519     }
520     
521     
522     
523     function toEmailCalcAddress($changes, $person)
524     {
525         $ff = HTML_FlexyFramework::get();
526      
527         
528         $headers['To'] = '"'. addslashes($person->name). '" <' . $person->email .'>';
529         $headers['From'] = $ff->MTrackWeb['email_address']; // could be the user who made the change...
530         
531         //if (count($from) > 1) {
532         //    $rep = array();
533         //    array_shift($from);
534          //       foreach ($from as $email) {
535          // $rep[] = make_email($email[0], $email[1]);
536         // }
537         
538         $headers['Reply-To'] =  $ff->MTrackWeb['email_address'];
539         return $headers;
540     }
541     
542     function images()
543     {
544         $i = DB_DataObject::factory('Images');
545         return $i->gather($this);
546     }
547     
548     function toEventName()
549     {
550         return $this->summary;
551         
552     }
553     
554     
555     function toEmail($person, $lasttime)
556     {
557         $ff = HTML_FlexyFramework::get(); // used to work out url to show..
558         
559         $CS = DB_DataObject::factory('mtrack_change');
560         $changes = $CS->changesSince($this,$lasttime);
561         $headers = $this->toEmailCalcAddress($changes, $person);
562         
563         $headers += array(
564             'MIME-Version' => '1.0',
565             'Content-Type' => 'text/plain; charset="UTF-8"',
566             'Content-Transfer-Encoding' => 'quoted-printable',
567         );
568     
569         
570         $mid = $this->id . '@' . php_uname('n');
571         
572         $p = $this->project();
573          
574         $subj = "[" . $p->code . ' - ' . $p->name . "] ";
575     
576         $headers['X-mtrack-project-list'] = $p->code;
577      
578     
579         $headers['Subject'] = sprintf("%s#%s %s (%s %s)",
580           $subj, $this->id, $this->summary, $this->status_name, $this->classification_id_name);
581     
582         $owner =  $this->owner_id ? $this->owner_id_name : 'nobody';
583     
584         $body = 
585             sprintf("%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id) .
586             sprintf("#%s: %s (%s %s)\n", $this->id, $this->summary, $this->status_name, $this->classification_id_name) .
587             sprintf("Responsible: %s (%s / %s)\n", $owner, $this->priority_id_name, $this->severity_id_name) .
588     
589             sprintf("\nDescription:\n%s\n", wordwrap($this->description,80, "\n")) .
590      //sprintf("Milestone: %s\n", join(', ', $T->getMilestones()));
591             //sprintf("Component: %s\n", join(', ', $T->getComponents()));
592             "\n";
593     
594       // Display changed fields grouped by the person that last changed them
595         //$who_changed = array();
596         //foreach ($field_changers as $field => $who) {
597         //  $who_changed[$who][] = $field;
598         //}
599         $body.="\n";
600         foreach ($changes as $change) {
601             
602             $body .= "\n{$change->changedate}:  Change by {$change->person_id_name}\n";
603             $body .= str_repeat('-', 80) . "\n";
604             if (!empty($change->reason)) {
605                 $body.= wordwrap($change->reason, 80, "\n") ."\n\n";
606             }
607             $ar = $change->cachedAudit();
608             
609             foreach($ar as $audit) {
610                 
611                 switch($audit->field()) {
612                     case 'id':
613                         continue; //??? ignore?
614                     case '@components':
615                         continue;
616                    
617                     case '@milestones':
618                         continue;
619                      
620                     case '@keywords':
621                         continue;
622                    
623                     //case 'commit?'  
624                       
625                     default:
626                         if ($audit->oldvalue == $audit->value) {
627                             continue; // no change?!?!?
628                         }
629                         $oldvalue = $audit->oldvalue($change);
630                         $value = $audit->value($change);
631                         $field = $audit->field();
632                         
633                         $field  = preg_replace('/_id$/', '', $field);
634                         $field  = str_replace('/_/', ' ', $field);
635                         $lb = strpos($oldvalue,"\n") > -1 || strpos($value,"\n") > -1 ? "\n" : '';
636                         
637                         if (!strlen($oldvalue)) {
638                             $body .= "Set {$field} to: {$lb}{$value}\n";
639                             continue;
640                         }
641                         
642                         if (!strlen($value)) {
643                             $body .= "Removed {$field} - was: {$lb}{$oldvalue}\n";
644                             continue;
645                         }
646                         $body .= "Changed {$field} from : {$lb}{$oldvalue} {$lb}-> {$lb}{$value}\n";
647                         continue;  
648                 }
649             }
650         }
651         
652         $body .= sprintf("\n\n%s/Ticket/%s\n\n", $ff->MTrackWeb['url'], $this->id);
653         
654         return array(
655             'body' => $body,
656             'headers' => $headers
657         );
658          
659         
660         
661     }
662     
663     function toEventString()
664     {
665         return '#'.$this->id . ' ' . $this->summary;
666         
667     }
668     
669     var $_mtrack_change = false;
670     
671     function beforeUpdate($old, $req, $roo)
672     {
673         if (!empty($req['reason'])) {
674             $ch = DB_DataObject::factory('mtrack_change');
675             $ch->beginChange($this, $req['reason']);
676             $this->_mtrack_change= $ch;
677         }
678         // used by gitlive close ticket...
679         if (!empty($req['status_name']) && empty($req['status'])) {
680             $n = DB_DataObject::Factory('core_enum');
681             $n->name = $req['status_name'];
682             $n->whereAddIn('etype', array('ticketstate','resolution'),'string');
683             if (!$n->find(true)) {
684                 $roo->jerr("invalid status name");
685             }
686             $this->status = $n->id;
687             
688         }
689         if ($old->status != $this->status) {
690             // status has been changed. - check to see if it's really a resolution.
691             $n = DB_DataObject::Factory('core_enum');
692             if (!$this->status || !$n->get($this->status)) {
693                 $roo->jerr("status set invalid");
694             }
695             if ($n->etype != 'ticketstate') {
696                 if ($n->etype != 'resolution') {
697                     $roo->jerr("status set invalid");
698                 }
699                 $this->resolution_id = $n->id;
700                 $n = DB_DataObject::Factory('core_enum');
701                 $n->etype = 'ticketstate';
702                 $n->get('name', 'closed'); // no error checking!?!
703                 $this->status = $n->id;
704             }
705             
706         }
707         
708         
709         
710         
711         if (isset($req['_reorder'])) {
712             $old->updateSequence($req['_reorder']);
713             $roo->jok("UPDATED");
714         }
715         
716         
717     }
718     
719     function beforeInsert($req, $roo)
720     {
721         
722         if (isset($req['_bulk_update'])) {
723             $this->bulkUpdate($req,$roo);
724             $this->jerr("oops");
725         }
726         
727         if (!$this->status) {
728             $n = DB_DataObject::Factory('core_enum');
729             $n->etype = 'ticketstate';
730             $n->get('name', 'new'); // no error checking!?!
731             $this->status = $n->id;
732         }
733     }
734     
735     
736     function bulkUpdate($req, $roo)
737     {
738         if (empty($req['_bulk_update'])) {
739             $roo->jerr("invalid tickets");
740         }
741         
742         $t = DB_DAtaObject::Factory('mtrack_ticket');
743         $t->autoJoin();
744         $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
745         $all = $t->fetchAll();
746         
747         foreach($req as $k=>$v) {
748             if (empty($v)) {
749                 continue;
750             }
751             $cg[$k] = $v;
752         }
753         foreach($all as $t) {
754             $old = clone($t);
755             $t->setFrom($cg);
756             $t->beforeUpdate($old, $cg, $roo);
757             $t->update($old);
758             $t->onUpdate($old,$req, $roo);
759             
760         }
761         $t = DB_DAtaObject::Factory('mtrack_ticket');
762         $t->autoJoin();
763         $t->whereAddIn($this->tableName().'.id', explode(',',$req['_bulk_update'] ), 'int');
764         $roo->jok($t->fetchAll(false,false,'toArray'));
765         
766         
767          
768         
769         
770     }
771     
772     
773     
774     
775     function onUpdate($old,$req, $roo)
776     {
777         //print_r($old); print_r($this);
778         
779          //die("on update");
780         $ch = $this->_mtrack_change; 
781         if (!$ch) {
782             $ch = DB_DataObject::factory('mtrack_change');
783             $ch->beginChange($this, "Changed");
784             $this->_mtrack_change = $ch;
785         }
786         
787         $n = $ch->add($this, $old);
788         if (!$n && !$this->_mtrack_change) {
789             //die("deleting change");
790             $ch->delete(); // do not record the change..
791         } else { 
792         
793             $oo = clone($this);
794             $this->updated = $ch->id;
795             $this->update($oo);
796         }
797         // trigger emails..
798         
799     }
800     
801     
802     
803     function updated()
804     {
805         $ch = DB_DataObject::factory('mtrack_change');
806         $ch->get($this->updated ? $this->updated : $this->created);
807         return $ch;
808         
809     }
810     
811     
812     function toRooSingleArray($au, $req)
813     {
814         if ($this->id) {
815             return $this->toArray();
816         }
817         // empty!
818         $p = $this->factory('core_enum');
819         $p->etype='priority';
820         $p->get('name', 'normal');
821         
822         $s = $this->factory('core_enum');
823         $s->etype='severity';
824         $s->get('name', 'normal');
825         
826         $c = $this->factory('core_enum');
827         $c->etype='classification';
828         $c->get('name', 'enhancement');
829         
830         $this->severity_id = $s->id;
831         $this->classification_id = $c->id;
832         $this->priority_id = $p->id;
833         $ret = $this->toArray();
834         $ret = array_merge($ret, $p->toArray('priority_id_%s'));
835         $ret = array_merge($ret, $c->toArray('classification_id_%s'));
836         $ret = array_merge($ret, $s->toArray('severity_id_%s'));
837         
838          return $ret;
839         
840     }
841     
842     //  http://roojs.com/admin.php/Roo/Mtrack_ticket?_post=1&id=733&_reorder=0
843     function updateSequence($tbefore)
844     {
845         $tn = $this->tableName();
846         $t = DB_DataObject::factory('mtrack_ticket');
847         $tt = clone($t);
848         $t->seqid = 0;
849         //$t->project_id = $this->project_id;
850         $t->autoJoin();
851         $t->selectAdd();
852         // we still apply to closed tickets????
853         //$t->whereAdd("join_status_id.name != 'closed'");
854         $t->applySort(false,  'old_style', false);
855         
856         $old_seqid = $this->seqid;
857         
858         if ($t->count()) {
859             // add a seq id to everything.
860             $t->selectAdd("{$tn}.id as id");
861             $ids = $t->fetchAll('id');
862             
863             $t_all = DB_DataObject::factory('mtrack_ticket');
864             $t_all->selectAdd('max(seqid) as seqid');
865             $t_all->find(true);
866             
867             $start = empty($t_all->seqid) ? 1 : $t_all->seqid;
868             $start++;
869             foreach($ids as $i => $id) {
870                 $tt->query("UPDATE {$tn} SET seqid = " . ($start + $i) ." WHERE id = {$id}");
871             }
872             $t = DB_DataObject::Factory('mtrack_ticket');
873             $t->get($this->id);
874             $old_seqid = $t->seqid;
875             
876         }
877         $before_seq_id = 0;
878         
879         if ($tbefore) { 
880             $before = DB_DataObject::factory('mtrack_ticket');
881             $before->get($tbefore);
882             $before_seq_id = $before->seqid;
883         }
884         
885         $t = DB_DataObject::factory('mtrack_ticket');
886         
887         
888         /// move everything down.
889         $t->query("UPDATE {$tn} SET
890                         seqid = seqid + 1
891                     WHERE
892                        seqid > {$before_seq_id}
893                 ");
894         
895         // ...1234 GAP 6789
896         
897         // puts this one in the gap..
898         $t->query("UPDATE {$tn} SET seqid = ". ( $before_seq_id  + 1) . " WHERE id = {$this->id}");
899         
900         // now there is a gap where this used to be..
901         
902         
903          $t->query("UPDATE {$tn} SET
904                         seqid = seqid - 1
905                    WHERE 
906                         seqid > {$old_seqid}
907         ");             
908          
909         
910     }
911     function toRooArray($q) {
912         if (empty($q['_future_schedule'])) {
913             return $this->toArray();
914         }
915         if (!$this->developer_id) {
916             return false;
917         }
918         $add = $this->toCalEvent();
919         if (isset($q['filter'])) {
920             foreach($q['filter'] as $k=>$v) {
921                 if (empty($v)) {
922                     continue;
923                 }
924                 if (!isset($add[$k])) {
925                     $roo->jerr('invalid filter ' .$k);
926                 }
927                 if ($v != $add[$k]) {
928                     $add = false;
929                 }
930             }
931         }
932         return $add;
933      
934     }
935      
936     
937     
938     // only run this once..
939     function toCalEvent()
940     {
941         //secho '<PRE>';print_r($this);exit;
942         static $cdata = array();
943         
944         
945         if (empty($cdata[$this->developer_id])) {
946             $start = $this->availStart(date('Y-m-d H:i:s'));
947         } else {
948             $start = $this->availStart($cdata[$this->developer_id]);
949         }
950         
951         $end = $this->availEnd($start, $this->estimated ? $this->estimated : 1);
952         $cdata[$this->developer_id]= $end;
953         
954         $ret = array_merge($this->toArray(), array(
955             'start_dt' => $start,
956             'end_dt' =>  $end,
957             //'start_time' => 
958             'title' => '#' . $this->id . '  ' .$this->developer_id_name . ' : ' . $this->project_id_name,
959             'description' => $this->summary,
960           
961         ));
962         return $ret;
963         
964         
965         
966     }
967     function availStart($dt)
968     {
969         //give a date time.. see if we should schedule it today?
970         $st = strtotime($dt);
971         
972         if (date('H', $st) < 11) {
973             $st = strtotime(date('Y-m-d 10:00:00',$st));
974         }
975         if (date('H', $st) > 17) {
976             if(date('N',$st) > 4) {
977                 // it's friday... (5) +3 , sat (6) +2 sun(7) +1
978                 $days = 8 - date('N',$st);
979                 $st = strtotime(date('Y-m-d',$st).' + ' . $days  . ' DAY');
980                 
981             } else {
982                 $st = strtotime(date('Y-m-d',$st).' + 1 DAY');
983             }
984             $st = strtotime(date('Y-m-d 10:00:00',$st));
985             return date('Y-m-d H:i:s', $st);
986         }
987         // otherwise this time is OK...
988         return $dt;
989     }
990     function availEnd($start, $hours) {
991         
992         $et = $start;
993         for ($i = 0; $i < ceil($hours ); $i++) {
994             $et = $this->availStart(date('Y-m-d H:i:s', strtotime($et . ' + 1 HOUR')));
995         }
996         return $et;
997         
998         
999     }
1000     
1001     function optionsForProject($roo,$pid)
1002     {
1003         $ret = array();
1004         $m = DB_DataObject::factory('mtrack_milestone');
1005         $m->project_id = $pid;
1006         $m->orderBy('startdate ASC');
1007         $ret['milestone'] =  $m->fetchAll(false,false,'toArray');
1008         
1009         $mm = DB_DataObject::factory('core_enum');
1010         $mm->orderBy('seqid ASC');
1011         $mm->active = 1;
1012         
1013         $m = clone($mm);
1014         $m->etype = 'priority';
1015         $ret['priority'] =  $m->fetchAll(false,false,'toArray');
1016         
1017         $m = clone($mm);
1018         $m->etype = 'severity';
1019         $ret['severity'] =  $m->fetchAll(false,false,'toArray');
1020         
1021         $m = clone($mm);
1022         $m->etype = 'classification';
1023         $ret['classification'] =  $m->fetchAll(false,false,'toArray');
1024
1025         $m = DB_DataObject::factory('core_person');
1026         $m->active = 1;
1027         $m->applyFilters(array(
1028             'query' => array(
1029                 'project_id' => $pid,
1030                 'role' => 'DEVELOPER'
1031             )
1032         ));
1033                                                 
1034         $m->orderBy('name ASC');
1035         $ret['developer'] =  $m->fetchAll(false,false,'toArray');
1036         
1037   
1038 /*
1039 _dc: 1545023830927
1040 start: 0
1041 limit: 20
1042 query[project_id]: 3116
1043 query[role]: DEVELOPER
1044 query[name]: 
1045 sort: name
1046 dir: ASC
1047     */   
1048     }
1049     
1050     
1051      
1052     
1053     
1054     
1055     
1056     
1057     
1058 }