import
[web.mtrack] / web / ticket.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3 include '../inc/common.php';
4
5 if ($pi = mtrack_get_pathinfo()) {
6   $id = $pi;
7 } else {
8   $id = $_GET['id'];
9 }
10
11 if ($id == 'new') {
12   $issue = new MTrackIssue;
13   $issue->priority = 'normal';
14 } else {
15   if (strlen($id) == 32) {
16     $issue = MTrackIssue::loadById($id);
17   } else {
18     $issue = MTrackIssue::loadByNSIdent($id);
19   }
20   if (!$issue) {
21     throw new Exception("Invalid ticket $id");
22   }
23 }
24
25 $FIELDSET = array(
26     array(
27       "description" => array(
28         "label" => "Full description",
29         "ownrow" => true,
30         "type" => "wiki",
31         "rows" => 10,
32         "cols" => 78,
33         "editonly" => true,
34         ),
35       ),
36     "Properties" => array(
37       "milestone" => array(
38         "label" => "Milestone",
39         "type" => "multiselect",
40         ),
41       "component" => array(
42         "label" => "Component",
43         "type" => "multiselect",
44         ),
45       "classification" => array(
46         "label" => "Classification",
47         "type" => "select",
48         ),
49       "priority" => array(
50         "label" => "Priority",
51         "type" => "select",
52         ),
53       "severity" => array(
54         "label" => "Severity",
55         "type" => "select",
56         ),
57       "keywords" => array(
58           "label" => "Keywords",
59           "type" => "text",
60           ),
61       "changelog" => array(
62           "label" => "ChangeLog (customer visible)",
63           "type" => "multi",
64           "ownrow" => true,
65           "rows" => 5,
66           "cols" => 78,
67        #   "condition" => $issue->status == 'closed'
68           ),
69       ),
70       "Resources" => array(
71           "owner" => array(
72             "label" => "Responsible",
73             "type" => "select"
74             ),
75           "estimated" => array(
76             "label" => "Estimated Hours",
77             "type" => "text"
78             ),
79           "spent" => array(
80             "label" => "Spent Hours",
81             "type" => "text",
82             "readonly" => true,
83             ),
84           "cc" => array(
85             "label" => "Cc",
86             "type" => "text"
87             ),
88           ),
89       );
90 $issue->augmentFormFields($FIELDSET);
91
92
93 $preview = false;
94 $error = array();
95
96 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
97   if (isset($_POST['cancel'])) {
98     header("Location: {$ABSWEB}ticket.php/$issue->nsident");
99     exit;
100   }
101   if (!MTrackCaptcha::check('ticket')) {
102     $error[] = "CAPTCHA failed, please try again";
103   }
104   $preview = isset($_POST['preview']) ? true : false;
105
106   $comment = '';
107   try {
108     if ($id == 'new') {
109       MTrackACL::requireAllRights("Tickets", 'create');
110     } else {
111       MTrackACL::requireAllRights("ticket:" . $issue->tid, 'modify');
112     }
113   } catch (Exception $e) {
114     $error[] = $e->getMessage();
115   }
116   if ($id == 'new') {
117     $comment = $_POST['comment'];
118   }
119   if (!strlen($comment)) {
120     $comment = $_POST['summary'];
121   }
122   try {
123     $CS = MTrackChangeset::begin("ticket:X", $comment);
124   } catch (Exception $e) {
125     $error[] = $e->getMessage();
126     $CS = null;
127   }
128   if ($id == 'new') {
129     // compute next id number.
130     // We don't use auto-number, because we allow for importing multiple
131     // projects with their own ticket sequence.
132     // During "normal" user-driven operation, we do want plain old id numbers
133     // so we compute it here, under a transaction
134     $db = MTrackDB::get();
135     if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
136         // Some versions of postgres don't like that we have "abc123" for
137         // identifiers, so match on the bigest number nsident fields only
138       $max = "select max(cast(nsident as integer)) + 1 from tickets where nsident ~ '^\\\\d+$'";
139     } else {
140       $max = 'select max(cast(nsident as integer)) + 1 from tickets';
141     }
142     list($issue->nsident) = MTrackDB::q($max)->fetchAll(PDO::FETCH_COLUMN, 0);
143     if ($issue->nsident === null) {
144       $issue->nsident = 1;
145     }
146   }
147
148   if (isset($_POST['action']) && !$preview) {
149     switch ($_POST['action']) {
150       case 'leave':
151         break;
152       case 'reopen':
153         $issue->reOpen();
154         break;
155       case 'fixed':
156         $issue->resolution = 'fixed';
157         $issue->close();
158         $_POST['estimated'] = $issue->estimated;
159         break;
160       case 'resolve':
161         $issue->resolution = $_POST['resolution'];
162         $issue->close();
163         $_POST['estimated'] = $issue->estimated;
164         break;
165       case 'accept':
166         // will be applied to the issue further down
167         $_POST['owner'] = MTrackAuth::whoami();
168         if ($issue->status == 'new') {
169           $issue->status = 'open';
170         }
171         break;
172       case 'changestatus':
173         $issue->status = $_POST['status'];
174         break;
175     }
176   }
177
178   $fields = array(
179     'summary',
180     'description',
181     'classification',
182     'priority',
183     'severity',
184     'changelog',
185     'owner',
186     'cc',
187   );
188
189   $issue->applyPOSTData($_POST);
190
191   foreach ($fields as $fieldname) {
192     if (isset($_POST[$fieldname]) && strlen($_POST[$fieldname])) {
193       $issue->$fieldname = $_POST[$fieldname];
194     } else {
195       $issue->$fieldname = null;
196     }
197   }
198
199   $kw = $issue->getKeywords();
200   $kill = array_values($kw);
201   foreach (preg_split('/[ \t,]+/', $_POST['keywords']) as $w) {
202     if (!strlen($w)) {
203       continue;
204     }
205     $x = array_search($w, $kw);
206     if ($x === false) {
207       $k = MTrackKeyword::loadByWord($w);
208       if ($k === null) {
209         $k = new MTrackKeyword;
210         $k->keyword = $w;
211         $k->save($CS);
212       }
213       $issue->assocKeyword($k);
214     } else {
215       $w = array_search($w, $kill);
216       if ($w !== false) {
217         unset($kill[$w]);
218       }
219     }
220   }
221   foreach ($kill as $w) {
222     $issue->dissocKeyword($w);
223   }
224
225   $ms = $issue->getMilestones();
226   $kill = $ms;
227   if (isset($_POST['milestone']) && is_array($_POST['milestone'])) {
228     foreach ($_POST['milestone'] as $mid) {
229       $issue->assocMilestone($mid);
230       unset($kill[$mid]);
231     }
232   }
233   foreach ($kill as $mid) {
234     $issue->dissocMilestone($mid);
235   }
236
237   $ms = $issue->getComponents();
238   $kill = $ms;
239   if (isset($_POST['component']) && is_array($_POST['component'])) {
240     foreach ($_POST['component'] as $mid) {
241       $issue->assocComponent($mid);
242       unset($kill[$mid]);
243     }
244   }
245   foreach ($kill as $mid) {
246     $issue->dissocComponent($mid);
247   }
248
249   $issue->addComment($_POST['comment']);
250   $issue->addEffort($_POST['spent'], $_POST['estimated']);
251
252   if (!count($error)) {
253     try {
254       $issue->save($CS);
255       $CS->setObject("ticket:" . $issue->tid);
256     } catch (Exception $e) {
257       $error[] = $e->getMessage();
258     }
259   }
260
261   if (!count($error)) {
262     if (isset($_FILES['attachments']) && is_array($_FILES['attachments'])) {
263       foreach ($_FILES['attachments']['name'] as $fileid => $name) {
264         MTrackAttachment::add("ticket:$issue->tid",
265             $_FILES['attachments']['tmp_name'][$fileid],
266             $_FILES['attachments']['name'][$fileid],
267             $CS);
268       }
269     }
270   }
271   if (!count($error) && $id != 'new') {
272     MTrackAttachment::process_delete("ticket:$issue->tid", $CS);
273   }
274
275   if (isset($_POST['apply']) && !count($error)) {
276     $CS->commit();
277     header("Location: {$ABSWEB}ticket.php/$issue->nsident");
278     exit;
279   }
280 }
281
282 if ($id == 'new') {
283   MTrackACL::requireAllRights("Tickets", 'create');
284   mtrack_head("New ticket");
285 } else {
286   MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
287   if ($issue->nsident) {
288     mtrack_head("#$issue->nsident " . $issue->summary);
289   } else {
290     mtrack_head("#$id " . $issue->summary);
291   }
292 }
293
294 echo "<form id='tktedit' method='post' action='{$ABSWEB}ticket.php/$id' enctype='multipart/form-data'>\n";
295 /* now to render the edit controls, if suitably privileged */
296 if ($id == 'new') {
297   $editable = MTrackACL::hasAllRights("Tickets", 'create');
298 } else {
299   $editable = MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify');
300 }
301
302 echo <<<HTML
303 <div id="issue-desc">
304 HTML;
305
306 if ($preview) {
307   echo <<<HTML
308 <div class='ui-state-highlight ui-corner-all'>
309     <span class='ui-icon ui-icon-info'></span>
310     This is a preview of your pending changes.  It does not show
311     changes to the resolution; those will be applied when you submit.
312 </div>
313
314 HTML;
315 }
316 if (count($error)) {
317   foreach ($error as $e) {
318     echo <<<HTML
319 <div class='ui-state-error ui-corner-all'>
320     <span class='ui-icon ui-icon-alert'></span>
321 HTML;
322     echo htmlentities($e, ENT_QUOTES, 'utf-8') . "\n</div>\n";
323   }
324 }
325
326 if ($id != 'new') {
327   echo "<h1>";
328   if (!$issue->isOpen()) {
329     echo "<del>";
330   }
331   if ($issue->nsident) {
332     echo "#$issue->nsident ";
333   } else {
334     echo "#$id ";
335   }
336
337   echo htmlentities($issue->summary, ENT_QUOTES, 'utf-8');
338
339   if (!$issue->isOpen()) {
340     echo "</del>";
341   }
342   echo "</h1>\n";
343 }
344
345 if ($id == 'new') {
346   $created = new stdClass;
347   $created->when = MTrackDB::unixtime(time());
348   $created->who = MTrackAuth::whoami();
349 } else {
350   $created = MTrackChangeset::get($issue->created);
351 }
352
353 $opened = mtrack_date($created->when);
354 echo <<<HTML
355 <div id="ticketinfo">
356 HTML;
357
358 $pseudo_fields = array();
359
360 if ($id != 'new') {
361   echo "<table id='ctime'><tr><td><label>Opened</label>:</td><td>",
362        mtrack_date($created->when),
363        "</td><td>",
364        mtrack_username($created->who, array('no_image' => true)),
365        "</td></tr>\n";
366   if ($issue->updated != $issue->created) {
367     $updated = MTrackChangeset::get($issue->updated);
368     echo "<tr><td><label>Updated</label>:</td><td>",
369       mtrack_date($updated->when),
370       "</td><td>",
371       mtrack_username($updated->who, array('no_image' => true)),
372       "</td></tr>";
373   }
374   echo "</table>";
375
376   $v = get_components_list(join(',', array_keys($issue->getComponents())));
377   $pseudo_fields['@components'] = $v;
378
379   $v = get_milestones_list(join(',', array_keys($issue->getMilestones())));
380   $pseudo_fields['@milestones'] = $v;
381
382   $v = get_keywords_list(join(',', array_keys($issue->getKeywords())));
383   $pseudo_fields['@keywords'] = $v;
384
385   $ROFIELDSET = $FIELDSET;
386   $ROFIELDSET['Properties']['resolution'] = array(
387     'label' => 'Resolution',
388     'type' => 'text',
389   );
390
391   foreach ($ROFIELDSET as $fsid => $fieldset) {
392     $emited = false;
393     foreach ($fieldset as $propname => $info) {
394       if (isset($info['editonly'])) {
395         continue;
396       }
397       $value = null;
398       switch ($propname) {
399         case 'keywords':
400           $value = array();
401           foreach ($issue->getKeywords() as $kw) {
402             $value[] = mtrack_keyword($kw);
403           }
404           $value = join(' ', $value);
405           break;
406         case 'milestone':
407           $value = $pseudo_fields['@milestones'];
408           break;
409         case 'component':
410           $value = $pseudo_fields['@components'];
411           break;
412         default:
413           $value = $issue->$propname;
414       }
415
416       if (strlen($value)) {
417         if (!$emited) {
418           $rfsid = 'readonly-tkt-' .
419             preg_replace('/[^a-z]+/', '', strtolower($fsid));
420           echo "<fieldset id='$rfsid'><legend>$fsid</legend>\n<table>";
421           $emited = true;
422         }
423
424         switch ($info['type']) {
425           case 'wiki':
426             $value = MTrackWiki::format_to_html($value);
427             break;
428           case 'multi':
429             $value = nl2br(htmlentities($value, ENT_QUOTES, 'utf-8'));
430             break;
431         }
432
433         if (isset($info['ownrow']) && $info['ownrow'] == 'true') {
434           echo "<tr><td colspan='2'><label>$info[label]</label>:</td></tr>";
435           echo "<td colspan='2'>$value</td></tr>\n";
436         } else {
437           echo "<tr><td><label>$info[label]</label>:</td><td width='100%'>$value</td></tr>\n";
438         }
439       }
440     }
441     if ($emited) {
442       echo "</table></fieldset>\n";
443     }
444   }
445 }
446 echo "</div>\n";
447
448 if ($issue->tid !== null) {
449   echo MTrackAttachment::renderList("ticket:$issue->tid");
450 }
451
452 if ($id != 'new') {
453   echo "<div id='readonly-tkt-description'>";
454   echo MTrackWiki::format_to_html($issue->description);
455   echo "</div>";
456 }
457
458 if ($editable && $id != 'new' && !$preview) {
459   echo "<br><div id='tkt-view-button-block' class='button-float'>";
460   echo "<button class='mtrack-edit-desc'>Edit</button>";
461   echo " <button class='mtrack-make-comment'>Add Comment</button>";
462   MTrackWatch::renderWatchUI('ticket', $issue->tid);
463   echo "</div>";
464 }
465 echo "</div>"; # issue-desc
466
467 $hide_unless_preview = ($preview || $_SERVER['REQUEST_METHOD'] == 'POST') ?
468   '' :
469   ' style="display:none" ';
470
471 if ($editable && $id != 'new') {
472   echo <<<HTML
473 <div id="edit-issue-desc" $hide_unless_preview >
474 HTML;
475 }
476
477 if ($editable) {
478
479   echo " <input class='summaryedit' id='summary' name='summary' value='" .
480     htmlentities($issue->summary, ENT_QUOTES, 'utf-8') .
481     "' size='80'>";
482
483   echo renderEditForm($issue);
484 }
485
486 if ($editable && $id != 'new') {
487   echo "</div>";
488 }
489
490
491 function get_components_list($value)
492 {
493   $res = array();
494   if (strlen($value)) foreach (MTrackDB::q(
495       "select name, deleted from components where compid in ($value)")
496       ->fetchAll() as $row) {
497     $c = $row['deleted'] ? '<del>' : '';
498     $c .= htmlentities($row['name'], ENT_QUOTES, 'utf-8');
499     $c .= $row['deleted'] ? '</del>' : '';
500     $res[] = $c;
501   }
502   return join(", ", $res);
503 }
504
505 function get_milestones_list($value)
506 {
507   global $ABSWEB;
508
509   $res = array();
510   if (strlen($value)) foreach (MTrackDB::q(
511       "select name, completed, deleted from milestones where mid in ($value)")
512       ->fetchAll() as $row) {
513     if (strlen($row['completed'])) {
514       $row['deleted'] = 1;
515     }
516     $c = "<span class='milestone";
517     if ($row['deleted']) {
518       $c .= " completed";
519     }
520     $c .= "'><a href=\"{$ABSWEB}milestone.php/" .
521           urlencode($row['name']) . '">';
522     $c .= htmlentities($row['name'], ENT_QUOTES, 'utf-8');
523     $c .= "</a></span>";
524     $res[] = $c;
525   }
526   return join(", ", $res);
527 }
528
529 function get_keywords_list($value)
530 {
531   $res = array();
532   if (strlen($value)) foreach (MTrackDB::q(
533       "select keyword from keywords where kid in ($value)")
534       ->fetchAll() as $row) {
535     $res[] = htmlentities($row['keyword'], ENT_QUOTES, 'utf-8');
536   }
537   return join(", ", $res);
538 }
539
540 if ($id == 'new') {
541   $changes = array();
542 } else {
543   $changes = array();
544   $cids = array();
545   foreach (MTrackDB::q(
546         'select * from changes where object = ?
547         order by changedate asc',
548         "ticket:$issue->tid")->fetchAll(PDO::FETCH_OBJ) as $CS) {
549     $changes[$CS->cid] = $CS;
550     $cids[] = $CS->cid;
551   }
552   $cidlist = join(',', $cids);
553
554   $change_audit = array();
555   foreach (MTrackDB::q("select * from change_audit where cid in ($cidlist)")
556       ->fetchAll(PDO::FETCH_ASSOC) as $citem) {
557     $change_audit[$citem['cid']][] = $citem;
558   }
559
560   /* also need to include cases where the ticket was modified as a side-effect
561    * of other manipulations (such as milestones being closed and tickets being
562    * re-targeted.  Such manipulations do not directly reference this ticket,
563    * and so do not need to be included in the effort_audit array that is
564    * populated below. */
565
566   $tid = $issue->tid;
567   foreach (MTrackDB::q(
568     "select c.cid as cid, c.who as who, c.object as object, c.changedate as changedate, c.reason as reason, ca.fieldname as fieldname, ca.action as action, ca.oldvalue as oldvalue, ca.value as value from change_audit ca left join changes c on (ca.cid = c.cid) where ca.cid not in ($cidlist) and ca.fieldname like 'ticket:$tid:%'")
569     ->fetchAll(PDO::FETCH_OBJ) as $CS) {
570     if (!isset($changes[$CS->cid])) {
571       $changes[$CS->cid] = $CS;
572     }
573     $change_audit[$CS->cid][] = array(
574       'cid' => $CS->cid,
575       'fieldname' => $CS->fieldname,
576       'action' => $CS->action,
577       'oldvalue' => $CS->oldvalue,
578       'value' => $CS->value
579       );
580   }
581
582   $effort_audit = array();
583   foreach (MTrackDB::q(
584       "select * from effort where cid in ($cidlist) and tid=?", $issue->tid)
585       ->fetchAll(PDO::FETCH_ASSOC) as $eff) {
586     $effort_audit[$eff['cid']][] = $eff;
587   }
588 }
589 ksort($changes);
590
591 $idno = 0;
592 $events = array();
593
594 function collapse_diff($diff)
595 {
596   static $idnum = 1;
597   $id = 'diff_' . $idnum++;
598   return "<br>" .
599     "<button onclick='\$(&quot;#$id&quot;).toggle(); return false;'>Toggle diff</button>".
600     "<div id='$id' style='display:none'>" .
601     mtrack_diff($diff) . "</div>";
602 }
603
604 foreach ($changes as $CS) {
605   $preamble = 0;
606   if ($idno == 0) {
607     $cid = "top";
608   } else {
609     $cid = "comment:$idno";
610   }
611   $idno++;
612
613   $who = $CS->who;
614   $timestamp = mtrack_date($CS->changedate, true);
615
616   $html = "<div class='ticketevent'><a class='pmark' href='#$cid'>#</a> <a name='$cid'>$timestamp</a> " .
617     mtrack_username($who, array('no_image' => true)) .
618     "</div>\n";
619
620   $html .= "<div class='ticketchangeinfo'>";
621   $html .= mtrack_username($who, array('no_name' => true, 'size' => 48));
622
623   if ($CS->cid == $issue->created) {
624     $html .= "<b>Opened</b><br>\n";
625   }
626
627   $comments = array();
628
629   if (is_array($change_audit[$CS->cid]))
630   foreach ($change_audit[$CS->cid] as $citem) {
631     list($tbl,,$field) = explode(':', $citem['fieldname'], 3);
632
633     if ($tbl != 'ticket') {
634       // can get here if we created a new keyword, for example
635       //var_dump($citem);
636       continue;
637     }
638     if ($field == '@comment') {
639       $comments[] = $citem['value'];
640       continue;
641     }
642
643     if ($field == '@components') {
644       $citem['value'] = get_components_list($citem['value']);
645     }
646     if ($field == '@milestones') {
647       $citem['value'] = get_milestones_list($citem['value']);
648     }
649     if ($field == '@keywords') {
650       $citem['value'] = get_keywords_list($citem['value']);
651     }
652     if ($field == 'spent') {
653       continue;
654     }
655     if ($field == 'estimated') {
656       if ($citem['value'] !== null) {
657         $citem['value'] += 0;
658       }
659       if ($citem['oldvalue'] !== null) {
660         $citem['oldvalue'] += 0;
661       }
662     }
663
664     if ($field[0] == '@') {
665       $main = isset($pseudo_fields[$field]) ? $pseudo_fields[$field] : '';
666       $field = substr($field, 1, -1);
667     } else {
668       $main = $issue->$field;
669     }
670
671     $f = MTrackTicket_CustomFields::getInstance()->fieldByName($field);
672     if ($f) {
673       $label = htmlentities($f->label, ENT_QUOTES, 'utf-8');
674     } else {
675       if ($field == 'attachment' && strlen($citem['oldvalue'])) {
676         $label = "Attachment: $citem[oldvalue]";
677       } else {
678         $label = ucfirst($field);
679       }
680     }
681
682     if ($citem['oldvalue'] == null) {
683       /* don't bother printing out a set if this is the initial thing
684        * and if the field values are currently the same */
685
686       if ($main != $citem['value'] || $cid != 'top') {
687
688         /* Special case for description; since it is multi-line and often
689          * very large, render it as a diff against the current ticket
690          * description field */
691         if ($field == 'description') {
692           if ($issue->description == $citem['value']) {
693             $html .= "<b>Description</b>: no longer empty; see above<br>";
694             continue;
695           }
696
697           $initial_lines = count(explode("\n", $issue->description));
698           $diff = mtrack_diff_strings($issue->description, $citem['value']);
699           $diff_add = 0;
700           $diff_rem = 0;
701           foreach (explode("\n", $diff) as $line) {
702             if (!strlen($line)) continue;
703             if ($line[0] == '-') {
704               $diff_rem++;
705             } else if ($line[0] == '+') {
706               $diff_add++;
707             }
708           }
709           if (abs($diff_add - $diff_rem) > $initial_lines / 2) {
710             $html .= "<b>initial $label</b><br>" .
711               MTrackWiki::format_to_html($citem['value']);
712           } else {
713             $diff = collapse_diff($diff);
714             $html .= "<b>initial $label</b> (diff to above):<br>$diff\n";
715           }
716         } else {
717           $html .= "<b>$label</b> $citem[value]<br>\n";
718         }
719       }
720     } elseif ($citem['action'] == 'changed') {
721       $lines = explode("\n", $citem['value'], 3);
722       if (count($lines) >= 2) {
723         $diff = mtrack_diff_strings($citem['oldvalue'], $citem['value']);
724         $diff = collapse_diff($diff);
725         $html .= "<b>$label</b> $citem[action]\n$diff\n";
726       } else {
727         $html .= "<b>$label</b> $citem[action] to $citem[value]<br>\n";
728       }
729     } else {
730       $html .= "<b>$label</b> $citem[action]<br>\n";
731     }
732   }
733
734   if (isset($effort_audit[$CS->cid]) && is_array($effort_audit[$CS->cid])) {
735     foreach ($effort_audit[$CS->cid] as $eff) {
736       $exp = (float)$eff['expended'];
737       if ($eff['expended'] != 0) {
738         $html .= "<b>spent</b> $exp hours<br>\n";
739         $preamble++;
740       }
741     }
742   }
743
744   if (count($comments)) {
745     if ($preamble) {
746       $html .= "<br>\n";
747       $preamble = 0;
748     }
749
750     foreach ($comments as $text) {
751       $html .= MTrackWiki::format_to_html($text);
752     }
753   }
754
755   $html .= "</div>"; # ticketchangeinfo
756
757   $events[] = $html;
758 }
759 if (count($events) > 80 && !isset($_GET['all'])) {
760   $num_hidden = count($events) - 20;
761   $turl = $ABSWEB . 'ticket.php/' . $issue->nsident . '?all=1';
762   echo <<<HTML
763 <br>
764 <div id='show-overflow' class='ui-state-highlight ui-corner-all'>
765     <span class='ui-icon ui-icon-info'></span>
766     There are $num_hidden older comments that are not shown.
767     <a class='button' href='$turl'>Show hidden comments</button>
768 </div>
769 HTML;
770 } else if (count($events) > 20 && !isset($_GET['all'])) {
771   $num_hidden = count($events) - 20;
772   echo <<<HTML
773 <br>
774 <div id='show-overflow' class='ui-state-highlight ui-corner-all'>
775     <span class='ui-icon ui-icon-info'></span>
776     There are $num_hidden older comments that are not shown.
777     <button id='button-show-overflow'>Show hidden comments</button>
778 </div>
779 <div id='ticketcommentsoverflow' style='display:none'>
780 HTML;
781   while (count($events) > 20) {
782     echo array_shift($events);
783   }
784   echo <<<HTML
785 </div>
786 <script type='text/javascript'>
787 $('#button-show-overflow').click(function() {
788   $('#show-overflow').hide('blind');
789   $('#ticketcommentsoverflow').show('clip');
790   return false;
791 });
792 </script>
793 HTML;
794
795 }
796 while (count($events)) {
797   echo array_shift($events);
798 }
799
800
801 if ($id != 'new') {
802 ?>
803 <br style="clear:both">
804 <button id="bottom-comment-button" class='mtrack-make-comment'>Add Comment</button>
805 <?php
806 }
807
808 ?>
809 </form>
810 <div id="confirmCancelDialog" style="display:none"
811     title="Are you sure?">
812   You've entered information into the form.
813   If you cancel, you will not be able to get it back.
814 </div>
815 <div id="noCommentDialog" style="display:none"
816     title="Please enter comment">
817   It seems you have not made any changes to the ticket,
818   and haven't entered any comments.
819 </div>
820 <div id="noSummaryDialog" style="display:none"
821     title="Please enter summary">
822   It seems you haven't entered a summary for the ticket.
823 </div>
824 <script type='text/javascript'>
825 var formChanged = <?php echo $preview ? 'true' : 'false'; ?>;
826
827 var viewblock;
828 var view_off;
829 var view_pos;
830 var editblock;
831 var edit_off;
832 var edit_pos;
833
834 function show_edit_form()
835 {
836   viewblock.css('position', view_pos);
837   viewblock.css('top', view_off.top);
838
839   $("#issue-desc").hide();
840   $("#edit-issue-desc").show();
841   $("#edit-comment-parent").append($("#comment-area"));
842   $("#comment-submit-buttons").hide();
843   $("#comment-area").show();
844   $(".mtrack-make-comment").hide();
845   $("#description").focus();
846
847   editblock.show();
848   edit_off = editblock.offset();
849   edit_pos = editblock.css('position');
850   viewblock.hide();
851   compute_floats();
852 }
853
854 function compute_floats()
855 {
856   if ($(viewblock).is(':visible')) {
857     if ($(this).scrollTop() > view_off.top) {
858       viewblock.css('position', 'fixed');
859       viewblock.css('top', '0px');
860       viewblock.addClass('button-float-floating');
861     } else {
862       viewblock.css('position', view_pos);
863       viewblock.css('top', view_off.top);
864       viewblock.removeClass('button-float-floating');
865     }
866   }
867   if ($(editblock).is(':visible')) {
868     if ($(this).scrollTop() > edit_off.top) {
869       editblock.css('position', 'fixed');
870       editblock.css('top', '0px');
871       editblock.addClass('button-float-floating');
872     } else if ($(this).scrollTop() < edit_off.top + editblock.height() - $(this).height()) {
873       editblock.css('position', 'fixed');
874       editblock.css('top', $(this).height() - editblock.outerHeight());
875       editblock.addClass('button-float-floating');
876     } else {
877       editblock.css('position', edit_pos);
878       editblock.css('top', edit_off.top);
879       editblock.removeClass('button-float-floating');
880     }
881   }
882 }
883
884 $(document).ready(function() {
885   viewblock = $('#tkt-view-button-block');
886   editblock = $('#tkt-edit-button-block');
887   view_off = viewblock.offset();
888   view_pos = viewblock.css('position');
889
890 $(window).scroll(function () {
891   compute_floats();
892 });
893
894
895 $(".mtrack-edit-desc").click(
896   function() {
897     show_edit_form();
898     return false;
899   }
900 );
901 $("input[type=radio]").click(
902   function() {
903     if (this.value == 'fixed') {
904       $("#changelog-container").show();
905     } else {
906       $("#changelog-container").hide();
907     }
908   }
909 );
910
911 $(":input").change(function() {
912   formChanged = true;
913 });
914 $("textarea").keyup(function() {
915   // This is here because IE doesn't seem to reliably trigger the
916   // change event with textareas
917   formChanged = true;
918 });
919 $("#confirmCancelDialog").dialog({
920   autoOpen: false,
921   bgiframe: true,
922   resizable: false,
923   modal: true,
924   buttons: {
925     'Discard': function() {
926       $(this).dialog('close');
927       cancel_form_changes();
928     },
929     'Keep': function() {
930       $(this).dialog('close');
931     }
932   }
933 });
934 $("#noCommentDialog").dialog({
935   autoOpen: false,
936   bgiframe: true,
937   resizable: false,
938   modal: true,
939   buttons: {
940     'OK': function() {
941       $(this).dialog('close');
942       $("#comment").focus();
943     }
944   }
945 });
946 $("#noSummaryDialog").dialog({
947   autoOpen: false,
948   bgiframe: true,
949   resizable: false,
950   modal: true,
951   buttons: {
952     'OK': function() {
953       $(this).dialog('close');
954       $("#summary").focus();
955     }
956   }
957 });
958
959 function cancel_form_changes()
960 {
961 <?php
962   if ($preview) {
963 ?>
964     document.location.href = document.location.href;
965     return false;
966 <?php
967   }
968 ?>
969   editblock.css('position', edit_pos);
970   editblock.css('top', edit_off.top);
971   editblock.hide();
972   viewblock.show();
973
974   $("#tktedit").each(function(){
975     // reset form
976     this.reset();
977   });
978   // notify asm select of change
979   $("select[multiple]").change();
980   $("#edit-issue-desc").hide();
981
982   $("#original-comment-parent").append($("#comment-area"));
983   $("#comment-submit-buttons").show();
984
985   <?php if ($id != 'new') { ?>
986   $("#comment-area").hide();
987   $(".mtrack-make-comment").show();
988   <?php } ?>
989   $("#issue-desc").show();
990   formChanged = false;
991   compute_floats();
992
993   return false;
994 }
995
996 $(".mtrack-edit-cancel").click(
997   function() {
998     if (formChanged) {
999       $("#confirmCancelDialog").dialog('open');
1000       return false;
1001     } else {
1002       return cancel_form_changes();
1003     }
1004   }
1005 );
1006 $(".mtrack-make-comment").click(
1007   function() {
1008     show_edit_form();
1009     $("#comment").focus();
1010     return false;
1011   }
1012 );
1013
1014 $(".mtrack-button-submit").click(function(){
1015
1016     var id = '<?php echo $id ?>';
1017
1018     if ($("#summary").val() == '') {
1019
1020         $("#summary").addClass('error');
1021         $("#noSummaryDialog").dialog('open');
1022         return false;
1023
1024     } else {
1025
1026         if (formChanged == false && $("#comment").val() == '') {
1027             $("#comment").addClass('error');
1028             $("#noCommentDialog").dialog('open');
1029             return false;
1030         }
1031
1032     }
1033
1034 });
1035
1036 $("#comment").keydown(function(){
1037     $("#comment").removeClass('error');
1038 });
1039
1040 $("#summary").keydown(function(){
1041     $("#summary").removeClass('error');
1042 });
1043
1044 <?php
1045 if ($issue->tid == null) {
1046 ?>
1047 $("#summary").focus();
1048 <?php
1049 }
1050 ?>
1051 });
1052 </script>
1053 <?php
1054
1055 mtrack_foot();
1056
1057 function renderEditForm($issue, $params = array())
1058 {
1059   global $id;
1060   global $ABSWEB;
1061   global $FIELDSET;
1062
1063   if (!isset($params['formname'])) {
1064     $params['formname'] = 'tktedit';
1065   } else if (!ctype_alpha($params['formname'])) {
1066     throw new Exception("invalid form name");
1067   }
1068
1069   /* compute allowed field values */
1070   $allowed = array();
1071
1072   $C = new MTrackClassification;
1073   $allowed['classification'] = $C->enumerate();
1074
1075   $P = new MTrackPriority;
1076   $allowed['priority'] = $P->enumerate();
1077
1078   $S = new MTrackSeverity;
1079   $allowed['severity'] = $S->enumerate();
1080
1081   $R = new MTrackResolution;
1082   $allowed['resolution'] = $R->enumerate();
1083
1084   $r = array();
1085   foreach (MTrackDB::q('select c.compid, c.name, p.name from components c left join components_by_project cbp on (c.compid = cbp.compid) left join projects p on (cbp.projid = p.projid) where deleted <> 1 order by c.name')
1086       ->fetchAll(PDO::FETCH_NUM) as $row) {
1087     if (strlen($row[2])) {
1088       $row[1] .= " ($row[2])";
1089     }
1090     $r[$row[0]] = $row[1];
1091   }
1092   $allowed['component'] = $r;
1093
1094   $r = array();
1095   foreach (MTrackDB::q(
1096         'select mid, name from milestones where deleted <> 1
1097         and completed is null order by (case when duedate is null then 1 else 0 end), duedate, name'
1098         )->fetchAll(PDO::FETCH_NUM) as $row) {
1099     $r[$row[0]] = $row[1];
1100   }
1101   foreach ($issue->getMilestones() as $mid => $name) {
1102     if (!isset($r[$mid])) {
1103       $r[$mid] = $name;
1104     }
1105   }
1106   $allowed['milestone'] = $r;
1107
1108   // FIXME: workflow should be able to influence this list of users
1109   $users = array();
1110   $inactiveusers = array();
1111   foreach (MTrackDB::q(
1112       'select userid, fullname, active from userinfo order by userid'
1113       )->fetchAll() as $row) {
1114     if (strlen($row[1])) {
1115       $disp = "$row[0] - $row[1]";
1116     } else {
1117       $disp = $row[0];
1118     }
1119     if ($row[2]) {
1120       $users[$row[0]] = $disp;
1121     } else {
1122       $inactiveusers[$row[0]] = $disp;
1123     }
1124   }
1125   $users[''] = 'nobody';
1126   // allow for inactive users to show up if they're currently responsible
1127   if (!isset($users[$issue->owner])) {
1128     if (!isset($inactiveusers[$issue->owner])) {
1129       $users[$issue->owner] = $issue->owner . ' (inactive)';
1130     } else {
1131       $users[$issue->owner] = $inactiveusers[$issue->owner] . ' (inactive)';
1132     }
1133   }
1134   // last ditch to have it show the right info
1135   if (!isset($users[$issue->owner])) {
1136     $users[$issue->owner] = $issue->owner;
1137   }
1138   $allowed['owner'] = $users;
1139
1140   $html = "<input type='hidden' name='tid' value='" .
1141     htmlentities($issue->tid === null ? 'new' : $issue->tid) . "'>\n";
1142
1143   /* render the form */
1144   $col = 0;
1145
1146   foreach ($FIELDSET as $fsid => $fieldset) {
1147     if (is_string($fsid)) {
1148       $html .= "<fieldset id='$fsid'><legend>$fsid</legend>\n";
1149     }
1150
1151     $html .= "<table class='fields'>";
1152     $col = 0;
1153     foreach ($fieldset as $propname => $info) {
1154       if (isset($info['readonly'])) {
1155         continue;
1156       }
1157       if (empty($info['ownrow'])) {
1158         $info['ownrow'] = false;
1159       }
1160       $value = null;
1161       switch ($propname) {
1162         case 'keywords':
1163           $value = join(' ', $issue->getKeywords());
1164           break;
1165         case 'milestone':
1166           $value = $issue->getMilestones();
1167           break;
1168         case 'component':
1169           $value = $issue->getComponents();
1170           break;
1171         case 'owner':
1172           $value = $issue->owner;
1173           if (!strlen($value)) {
1174             $value = '';
1175           }
1176           break;
1177         default:
1178           if (isset($issue->$propname)) {
1179             $value = $issue->$propname;
1180           }
1181       }
1182       if (isset($info['condition']) && !$info['condition']) {
1183         continue;
1184       }
1185
1186       if (($info['ownrow'] && $col) || $col == 2) {
1187         $html .= "</tr>\n";
1188         $col = 0;
1189       }
1190       if ($col == 0) {
1191         $html .= "<tr valign='top'>";
1192       }
1193       $col++;
1194       if ($info['ownrow']) {
1195         $html .= "<td colspan='4'>";
1196       } else if ($info['type'] == 'multiselect') {
1197         $html .= "<td colspan='2'>";
1198       } else {
1199         $html .= "<td>";
1200       }
1201
1202       if ($value === null && isset($info['default'])) {
1203         $value = $info['default'];
1204       }
1205
1206       if ($info['type'] != 'multiselect') {
1207         $html .= "<label for='$propname'>".
1208           htmlentities($info['label'], ENT_QUOTES, 'utf-8').
1209           ":</label>";
1210       }
1211
1212       if ($info['ownrow']) {
1213         $html .= "<br>\n";
1214       } else if ($info['type'] != 'multiselect') {
1215         $html .= "</td><td class='col$col'>";
1216       }
1217
1218       switch ($info['type']) {
1219         case 'text':
1220           $size = empty($info['size']) ? "" : "size='$info[size]' ";
1221           $html .= "<input id='$propname' name='$propname' ".
1222             $size .
1223             "value='".htmlentities($value, ENT_QUOTES, 'utf-8').
1224             "'>";
1225           break;
1226
1227         case 'multi':
1228           $html .= "<textarea id='$propname' name='$propname' ".
1229             "rows='$info[rows]' cols='$info[cols]' class='code'>".
1230             htmlentities($value, ENT_QUOTES, 'utf-8').
1231             "</textarea>\n";
1232           break;
1233
1234         case 'wiki':
1235           $srows = $info['rows'] + 1;
1236           $html .= "<textarea id='$propname' name='$propname' ".
1237             "style='height: {$srows}em' " .
1238             "rows='$info[rows]' cols='$info[cols]' class='code wiki'>".
1239             htmlentities($value, ENT_QUOTES, 'utf-8').
1240             "</textarea>\n";
1241           break;
1242
1243         case 'shortwiki':
1244           $html .= "<textarea id='$propname' name='$propname' ".
1245             "rows='$info[rows]' cols='$info[cols]' class='code wiki shortwiki'>"
1246             . htmlentities($value, ENT_QUOTES, 'utf-8').
1247             "</textarea>\n";
1248           break;
1249
1250         case 'select':
1251           if (isset($allowed[$propname])) {
1252             $html .= mtrack_select_box($propname,
1253                 $allowed[$propname], $value);
1254           } else {
1255             $html .= mtrack_select_box($propname,
1256                 $info['options'], $value);
1257           }
1258           break;
1259
1260         case 'multiselect':
1261           if (isset($allowed[$propname])) {
1262             $html .= mtrack_multi_select_box($propname,
1263                 $info['label'] . " (select to add)",
1264                 $allowed[$propname], $value);
1265           } else {
1266             $html .= mtrack_multi_select_box($propname,
1267                 $info['label'] . " (select to add)",
1268                 $info['options'], $value);
1269           }
1270           break;
1271       }
1272
1273       $html .= "</td>\n";
1274
1275       if ($info['ownrow']) {
1276         $html .= "</tr>\n";
1277         $col = 0;
1278       }
1279     }
1280     $html .= "</table>\n";
1281
1282     if (is_string($fsid)) {
1283       $html .= "</fieldset>\n";
1284     }
1285   }
1286
1287   $html .= "<fieldset id='action-container'><legend>Action</legend>\n";
1288
1289   // FIXME: workflow inspired actions listed here
1290   if (!isset($_POST['action'])) {
1291     $_POST['action'] = 'none';
1292   }
1293   if ($id != 'new') {
1294     $html .= mtrack_radio('action', 'none', $_POST['action']);
1295     $status = $issue->status == 'closed' ? $issue->resolution : $issue->status;
1296     $html .= " <label for='none'>Leave status as $status</label><br>\n";
1297
1298     if ($issue->status != 'closed') {
1299       $html .= mtrack_radio('action', 'accept', $_POST['action']);
1300       $html .= " <label for='accept'>Accept ticket</label><br>\n";
1301
1302       $ST = new MTrackTicketState;
1303       $ST = $ST->enumerate();
1304       unset($ST['closed']);
1305       unset($ST[$status]);
1306       if (count($ST)) {
1307         $html .= mtrack_radio('action', 'changestatus', $_POST['action']);
1308         $html .= " <label for='changestatus'>Change status to:</label>";
1309         $html .= mtrack_select_box('status', $ST, $issue->status);
1310         $html .= "<br>\n";
1311       }
1312
1313       $html .= mtrack_radio('action', 'fixed', $_POST['action']);
1314       $html .= " <label for='fixed'>Resolve as fixed</label><br>\n";
1315
1316       $R = new MTrackResolution;
1317       $resolutions = $R->enumerate();
1318       unset($resolutions['fixed']);
1319       $html .= mtrack_radio('action', 'resolve', $_POST['action']);
1320       $html .= " <label for='resolve'>Resolve as:</label>";
1321       $html .= mtrack_select_box('resolution', $resolutions);
1322       $html .= "<br>\n";
1323
1324     } else {
1325       $html .= mtrack_radio('action', 'reopen', $_POST['action']);
1326       $html .= " <label for='reopen'>Reopen ticket</label><br>\n";
1327     }
1328     $html .= "<br>\n";
1329   }
1330
1331   $spent = empty($_POST['spent']) ? '' : htmlentities($_POST['spent'], ENT_QUOTES, 'utf-8');
1332   if (!strlen($spent)) {
1333     $spent = '0';
1334   }
1335   $html .= "<label for='spent'>Log time spent (hours)</label> ";
1336   $html .= "<input type='text' name='spent' value='$spent'><br>\n";
1337
1338   if ($id != 'new') {
1339     $html .= MTrackAttachment::renderDeleteList("ticket:$issue->tid");
1340     $html .= <<<HTML
1341   <br>
1342   <label for='attachments[]'>Select file(s) to be attached</label>
1343   <input type='file' class='multi' name='attachments[]'>
1344 HTML;
1345   }
1346   $html .= "</fieldset>";
1347   $html .= "<fieldset id='comment-container'><legend>Comment</legend>\n";
1348
1349   $html .= <<<HTML
1350 <textarea name='comment' id="comment"
1351   class="wiki shortwiki" rows="5" cols="78">
1352 HTML;
1353   if (isset($_POST['comment'])) {
1354     $html .= htmlentities($_POST['comment'], ENT_QUOTES, 'utf-8');
1355   }
1356   $html .= "</textarea>";
1357
1358   $html .= "</fieldset>";
1359   $html .= MTrackCaptcha::emit('ticket');
1360
1361   $html .= <<<HTML
1362     <div id='tkt-edit-button-block' class='button-float'>
1363     <button class='mtrack-button-submit' type="submit" name="preview">Preview</button>
1364     <button class='mtrack-button-submit' type="submit" name="apply">Submit changes</button>
1365     <button class='mtrack-edit-cancel' type="submit" name="cancel">Cancel</button>
1366 HTML;
1367
1368   if ($id != 'new') {
1369     $html .= <<<HTML
1370 <button class='mtrack-make-comment'>Add Comment</button>
1371 HTML;
1372   }
1373
1374   $html .= "</div>";
1375
1376   return $html;
1377 }