1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3 include '../inc/common.php';
5 if ($pi = mtrack_get_pathinfo()) {
12 $issue = new MTrackIssue;
13 $issue->priority = 'normal';
15 if (strlen($id) == 32) {
16 $issue = MTrackIssue::loadById($id);
18 $issue = MTrackIssue::loadByNSIdent($id);
21 throw new Exception("Invalid ticket $id");
27 "description" => array(
28 "label" => "Full description",
36 "Properties" => array(
38 "label" => "Milestone",
39 "type" => "multiselect",
42 "label" => "Component",
43 "type" => "multiselect",
45 "classification" => array(
46 "label" => "Classification",
50 "label" => "Priority",
54 "label" => "Severity",
58 "label" => "Keywords",
62 "label" => "ChangeLog (customer visible)",
67 # "condition" => $issue->status == 'closed'
72 "label" => "Responsible",
76 "label" => "Estimated Hours",
80 "label" => "Spent Hours",
90 $issue->augmentFormFields($FIELDSET);
96 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
97 if (isset($_POST['cancel'])) {
98 header("Location: {$ABSWEB}ticket.php/$issue->nsident");
101 if (!MTrackCaptcha::check('ticket')) {
102 $error[] = "CAPTCHA failed, please try again";
104 $preview = isset($_POST['preview']) ? true : false;
109 MTrackACL::requireAllRights("Tickets", 'create');
111 MTrackACL::requireAllRights("ticket:" . $issue->tid, 'modify');
113 } catch (Exception $e) {
114 $error[] = $e->getMessage();
117 $comment = $_POST['comment'];
119 if (!strlen($comment)) {
120 $comment = $_POST['summary'];
123 $CS = MTrackChangeset::begin("ticket:X", $comment);
124 } catch (Exception $e) {
125 $error[] = $e->getMessage();
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+$'";
140 $max = 'select max(cast(nsident as integer)) + 1 from tickets';
142 list($issue->nsident) = MTrackDB::q($max)->fetchAll(PDO::FETCH_COLUMN, 0);
143 if ($issue->nsident === null) {
148 if (isset($_POST['action']) && !$preview) {
149 switch ($_POST['action']) {
156 $issue->resolution = 'fixed';
158 $_POST['estimated'] = $issue->estimated;
161 $issue->resolution = $_POST['resolution'];
163 $_POST['estimated'] = $issue->estimated;
166 // will be applied to the issue further down
167 $_POST['owner'] = MTrackAuth::whoami();
168 if ($issue->status == 'new') {
169 $issue->status = 'open';
173 $issue->status = $_POST['status'];
189 $issue->applyPOSTData($_POST);
191 foreach ($fields as $fieldname) {
192 if (isset($_POST[$fieldname]) && strlen($_POST[$fieldname])) {
193 $issue->$fieldname = $_POST[$fieldname];
195 $issue->$fieldname = null;
199 $kw = $issue->getKeywords();
200 $kill = array_values($kw);
201 foreach (preg_split('/[ \t,]+/', $_POST['keywords']) as $w) {
205 $x = array_search($w, $kw);
207 $k = MTrackKeyword::loadByWord($w);
209 $k = new MTrackKeyword;
213 $issue->assocKeyword($k);
215 $w = array_search($w, $kill);
221 foreach ($kill as $w) {
222 $issue->dissocKeyword($w);
225 $ms = $issue->getMilestones();
227 if (isset($_POST['milestone']) && is_array($_POST['milestone'])) {
228 foreach ($_POST['milestone'] as $mid) {
229 $issue->assocMilestone($mid);
233 foreach ($kill as $mid) {
234 $issue->dissocMilestone($mid);
237 $ms = $issue->getComponents();
239 if (isset($_POST['component']) && is_array($_POST['component'])) {
240 foreach ($_POST['component'] as $mid) {
241 $issue->assocComponent($mid);
245 foreach ($kill as $mid) {
246 $issue->dissocComponent($mid);
249 $issue->addComment($_POST['comment']);
250 $issue->addEffort($_POST['spent'], $_POST['estimated']);
252 if (!count($error)) {
255 $CS->setObject("ticket:" . $issue->tid);
256 } catch (Exception $e) {
257 $error[] = $e->getMessage();
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],
271 if (!count($error) && $id != 'new') {
272 MTrackAttachment::process_delete("ticket:$issue->tid", $CS);
275 if (isset($_POST['apply']) && !count($error)) {
277 header("Location: {$ABSWEB}ticket.php/$issue->nsident");
283 MTrackACL::requireAllRights("Tickets", 'create');
284 mtrack_head("New ticket");
286 MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
287 if ($issue->nsident) {
288 mtrack_head("#$issue->nsident " . $issue->summary);
290 mtrack_head("#$id " . $issue->summary);
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 */
297 $editable = MTrackACL::hasAllRights("Tickets", 'create');
299 $editable = MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify');
303 <div id="issue-desc">
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.
317 foreach ($error as $e) {
319 <div class='ui-state-error ui-corner-all'>
320 <span class='ui-icon ui-icon-alert'></span>
322 echo htmlentities($e, ENT_QUOTES, 'utf-8') . "\n</div>\n";
328 if (!$issue->isOpen()) {
331 if ($issue->nsident) {
332 echo "#$issue->nsident ";
337 echo htmlentities($issue->summary, ENT_QUOTES, 'utf-8');
339 if (!$issue->isOpen()) {
346 $created = new stdClass;
347 $created->when = MTrackDB::unixtime(time());
348 $created->who = MTrackAuth::whoami();
350 $created = MTrackChangeset::get($issue->created);
353 $opened = mtrack_date($created->when);
355 <div id="ticketinfo">
358 $pseudo_fields = array();
361 echo "<table id='ctime'><tr><td><label>Opened</label>:</td><td>",
362 mtrack_date($created->when),
364 mtrack_username($created->who, array('no_image' => true)),
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),
371 mtrack_username($updated->who, array('no_image' => true)),
376 $v = get_components_list(join(',', array_keys($issue->getComponents())));
377 $pseudo_fields['@components'] = $v;
379 $v = get_milestones_list(join(',', array_keys($issue->getMilestones())));
380 $pseudo_fields['@milestones'] = $v;
382 $v = get_keywords_list(join(',', array_keys($issue->getKeywords())));
383 $pseudo_fields['@keywords'] = $v;
385 $ROFIELDSET = $FIELDSET;
386 $ROFIELDSET['Properties']['resolution'] = array(
387 'label' => 'Resolution',
391 foreach ($ROFIELDSET as $fsid => $fieldset) {
393 foreach ($fieldset as $propname => $info) {
394 if (isset($info['editonly'])) {
401 foreach ($issue->getKeywords() as $kw) {
402 $value[] = mtrack_keyword($kw);
404 $value = join(' ', $value);
407 $value = $pseudo_fields['@milestones'];
410 $value = $pseudo_fields['@components'];
413 $value = $issue->$propname;
416 if (strlen($value)) {
418 $rfsid = 'readonly-tkt-' .
419 preg_replace('/[^a-z]+/', '', strtolower($fsid));
420 echo "<fieldset id='$rfsid'><legend>$fsid</legend>\n<table>";
424 switch ($info['type']) {
426 $value = MTrackWiki::format_to_html($value);
429 $value = nl2br(htmlentities($value, ENT_QUOTES, 'utf-8'));
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";
437 echo "<tr><td><label>$info[label]</label>:</td><td width='100%'>$value</td></tr>\n";
442 echo "</table></fieldset>\n";
448 if ($issue->tid !== null) {
449 echo MTrackAttachment::renderList("ticket:$issue->tid");
453 echo "<div id='readonly-tkt-description'>";
454 echo MTrackWiki::format_to_html($issue->description);
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);
465 echo "</div>"; # issue-desc
467 $hide_unless_preview = ($preview || $_SERVER['REQUEST_METHOD'] == 'POST') ?
469 ' style="display:none" ';
471 if ($editable && $id != 'new') {
473 <div id="edit-issue-desc" $hide_unless_preview >
479 echo " <input class='summaryedit' id='summary' name='summary' value='" .
480 htmlentities($issue->summary, ENT_QUOTES, 'utf-8') .
483 echo renderEditForm($issue);
486 if ($editable && $id != 'new') {
491 function get_components_list($value)
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>' : '';
502 return join(", ", $res);
505 function get_milestones_list($value)
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'])) {
516 $c = "<span class='milestone";
517 if ($row['deleted']) {
520 $c .= "'><a href=\"{$ABSWEB}milestone.php/" .
521 urlencode($row['name']) . '">';
522 $c .= htmlentities($row['name'], ENT_QUOTES, 'utf-8');
526 return join(", ", $res);
529 function get_keywords_list($value)
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');
537 return join(", ", $res);
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;
552 $cidlist = join(',', $cids);
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;
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. */
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;
573 $change_audit[$CS->cid][] = array(
575 'fieldname' => $CS->fieldname,
576 'action' => $CS->action,
577 'oldvalue' => $CS->oldvalue,
578 'value' => $CS->value
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;
594 function collapse_diff($diff)
597 $id = 'diff_' . $idnum++;
599 "<button onclick='\$("#$id").toggle(); return false;'>Toggle diff</button>".
600 "<div id='$id' style='display:none'>" .
601 mtrack_diff($diff) . "</div>";
604 foreach ($changes as $CS) {
609 $cid = "comment:$idno";
614 $timestamp = mtrack_date($CS->changedate, true);
616 $html = "<div class='ticketevent'><a class='pmark' href='#$cid'>#</a> <a name='$cid'>$timestamp</a> " .
617 mtrack_username($who, array('no_image' => true)) .
620 $html .= "<div class='ticketchangeinfo'>";
621 $html .= mtrack_username($who, array('no_name' => true, 'size' => 48));
623 if ($CS->cid == $issue->created) {
624 $html .= "<b>Opened</b><br>\n";
629 if (is_array($change_audit[$CS->cid]))
630 foreach ($change_audit[$CS->cid] as $citem) {
631 list($tbl,,$field) = explode(':', $citem['fieldname'], 3);
633 if ($tbl != 'ticket') {
634 // can get here if we created a new keyword, for example
638 if ($field == '@comment') {
639 $comments[] = $citem['value'];
643 if ($field == '@components') {
644 $citem['value'] = get_components_list($citem['value']);
646 if ($field == '@milestones') {
647 $citem['value'] = get_milestones_list($citem['value']);
649 if ($field == '@keywords') {
650 $citem['value'] = get_keywords_list($citem['value']);
652 if ($field == 'spent') {
655 if ($field == 'estimated') {
656 if ($citem['value'] !== null) {
657 $citem['value'] += 0;
659 if ($citem['oldvalue'] !== null) {
660 $citem['oldvalue'] += 0;
664 if ($field[0] == '@') {
665 $main = isset($pseudo_fields[$field]) ? $pseudo_fields[$field] : '';
666 $field = substr($field, 1, -1);
668 $main = $issue->$field;
671 $f = MTrackTicket_CustomFields::getInstance()->fieldByName($field);
673 $label = htmlentities($f->label, ENT_QUOTES, 'utf-8');
675 if ($field == 'attachment' && strlen($citem['oldvalue'])) {
676 $label = "Attachment: $citem[oldvalue]";
678 $label = ucfirst($field);
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 */
686 if ($main != $citem['value'] || $cid != 'top') {
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>";
697 $initial_lines = count(explode("\n", $issue->description));
698 $diff = mtrack_diff_strings($issue->description, $citem['value']);
701 foreach (explode("\n", $diff) as $line) {
702 if (!strlen($line)) continue;
703 if ($line[0] == '-') {
705 } else if ($line[0] == '+') {
709 if (abs($diff_add - $diff_rem) > $initial_lines / 2) {
710 $html .= "<b>initial $label</b><br>" .
711 MTrackWiki::format_to_html($citem['value']);
713 $diff = collapse_diff($diff);
714 $html .= "<b>initial $label</b> (diff to above):<br>$diff\n";
717 $html .= "<b>$label</b> $citem[value]<br>\n";
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";
727 $html .= "<b>$label</b> $citem[action] to $citem[value]<br>\n";
730 $html .= "<b>$label</b> $citem[action]<br>\n";
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";
744 if (count($comments)) {
750 foreach ($comments as $text) {
751 $html .= MTrackWiki::format_to_html($text);
755 $html .= "</div>"; # ticketchangeinfo
759 if (count($events) > 80 && !isset($_GET['all'])) {
760 $num_hidden = count($events) - 20;
761 $turl = $ABSWEB . 'ticket.php/' . $issue->nsident . '?all=1';
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>
770 } else if (count($events) > 20 && !isset($_GET['all'])) {
771 $num_hidden = count($events) - 20;
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>
779 <div id='ticketcommentsoverflow' style='display:none'>
781 while (count($events) > 20) {
782 echo array_shift($events);
786 <script type='text/javascript'>
787 $('#button-show-overflow').click(function() {
788 $('#show-overflow').hide('blind');
789 $('#ticketcommentsoverflow').show('clip');
796 while (count($events)) {
797 echo array_shift($events);
803 <br style="clear:both">
804 <button id="bottom-comment-button" class='mtrack-make-comment'>Add Comment</button>
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.
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.
820 <div id="noSummaryDialog" style="display:none"
821 title="Please enter summary">
822 It seems you haven't entered a summary for the ticket.
824 <script type='text/javascript'>
825 var formChanged = <?php echo $preview ? 'true' : 'false'; ?>;
834 function show_edit_form()
836 viewblock.css('position', view_pos);
837 viewblock.css('top', view_off.top);
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();
848 edit_off = editblock.offset();
849 edit_pos = editblock.css('position');
854 function compute_floats()
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');
862 viewblock.css('position', view_pos);
863 viewblock.css('top', view_off.top);
864 viewblock.removeClass('button-float-floating');
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');
877 editblock.css('position', edit_pos);
878 editblock.css('top', edit_off.top);
879 editblock.removeClass('button-float-floating');
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');
890 $(window).scroll(function () {
895 $(".mtrack-edit-desc").click(
901 $("input[type=radio]").click(
903 if (this.value == 'fixed') {
904 $("#changelog-container").show();
906 $("#changelog-container").hide();
911 $(":input").change(function() {
914 $("textarea").keyup(function() {
915 // This is here because IE doesn't seem to reliably trigger the
916 // change event with textareas
919 $("#confirmCancelDialog").dialog({
925 'Discard': function() {
926 $(this).dialog('close');
927 cancel_form_changes();
930 $(this).dialog('close');
934 $("#noCommentDialog").dialog({
941 $(this).dialog('close');
942 $("#comment").focus();
946 $("#noSummaryDialog").dialog({
953 $(this).dialog('close');
954 $("#summary").focus();
959 function cancel_form_changes()
964 document.location.href = document.location.href;
969 editblock.css('position', edit_pos);
970 editblock.css('top', edit_off.top);
974 $("#tktedit").each(function(){
978 // notify asm select of change
979 $("select[multiple]").change();
980 $("#edit-issue-desc").hide();
982 $("#original-comment-parent").append($("#comment-area"));
983 $("#comment-submit-buttons").show();
985 <?php if ($id != 'new') { ?>
986 $("#comment-area").hide();
987 $(".mtrack-make-comment").show();
989 $("#issue-desc").show();
996 $(".mtrack-edit-cancel").click(
999 $("#confirmCancelDialog").dialog('open');
1002 return cancel_form_changes();
1006 $(".mtrack-make-comment").click(
1009 $("#comment").focus();
1014 $(".mtrack-button-submit").click(function(){
1016 var id = '<?php echo $id ?>';
1018 if ($("#summary").val() == '') {
1020 $("#summary").addClass('error');
1021 $("#noSummaryDialog").dialog('open');
1026 if (formChanged == false && $("#comment").val() == '') {
1027 $("#comment").addClass('error');
1028 $("#noCommentDialog").dialog('open');
1036 $("#comment").keydown(function(){
1037 $("#comment").removeClass('error');
1040 $("#summary").keydown(function(){
1041 $("#summary").removeClass('error');
1045 if ($issue->tid == null) {
1047 $("#summary").focus();
1057 function renderEditForm($issue, $params = array())
1063 if (!isset($params['formname'])) {
1064 $params['formname'] = 'tktedit';
1065 } else if (!ctype_alpha($params['formname'])) {
1066 throw new Exception("invalid form name");
1069 /* compute allowed field values */
1072 $C = new MTrackClassification;
1073 $allowed['classification'] = $C->enumerate();
1075 $P = new MTrackPriority;
1076 $allowed['priority'] = $P->enumerate();
1078 $S = new MTrackSeverity;
1079 $allowed['severity'] = $S->enumerate();
1081 $R = new MTrackResolution;
1082 $allowed['resolution'] = $R->enumerate();
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])";
1090 $r[$row[0]] = $row[1];
1092 $allowed['component'] = $r;
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];
1101 foreach ($issue->getMilestones() as $mid => $name) {
1102 if (!isset($r[$mid])) {
1106 $allowed['milestone'] = $r;
1108 // FIXME: workflow should be able to influence this list of users
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]";
1120 $users[$row[0]] = $disp;
1122 $inactiveusers[$row[0]] = $disp;
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)';
1131 $users[$issue->owner] = $inactiveusers[$issue->owner] . ' (inactive)';
1134 // last ditch to have it show the right info
1135 if (!isset($users[$issue->owner])) {
1136 $users[$issue->owner] = $issue->owner;
1138 $allowed['owner'] = $users;
1140 $html = "<input type='hidden' name='tid' value='" .
1141 htmlentities($issue->tid === null ? 'new' : $issue->tid) . "'>\n";
1143 /* render the form */
1146 foreach ($FIELDSET as $fsid => $fieldset) {
1147 if (is_string($fsid)) {
1148 $html .= "<fieldset id='$fsid'><legend>$fsid</legend>\n";
1151 $html .= "<table class='fields'>";
1153 foreach ($fieldset as $propname => $info) {
1154 if (isset($info['readonly'])) {
1157 if (empty($info['ownrow'])) {
1158 $info['ownrow'] = false;
1161 switch ($propname) {
1163 $value = join(' ', $issue->getKeywords());
1166 $value = $issue->getMilestones();
1169 $value = $issue->getComponents();
1172 $value = $issue->owner;
1173 if (!strlen($value)) {
1178 if (isset($issue->$propname)) {
1179 $value = $issue->$propname;
1182 if (isset($info['condition']) && !$info['condition']) {
1186 if (($info['ownrow'] && $col) || $col == 2) {
1191 $html .= "<tr valign='top'>";
1194 if ($info['ownrow']) {
1195 $html .= "<td colspan='4'>";
1196 } else if ($info['type'] == 'multiselect') {
1197 $html .= "<td colspan='2'>";
1202 if ($value === null && isset($info['default'])) {
1203 $value = $info['default'];
1206 if ($info['type'] != 'multiselect') {
1207 $html .= "<label for='$propname'>".
1208 htmlentities($info['label'], ENT_QUOTES, 'utf-8').
1212 if ($info['ownrow']) {
1214 } else if ($info['type'] != 'multiselect') {
1215 $html .= "</td><td class='col$col'>";
1218 switch ($info['type']) {
1220 $size = empty($info['size']) ? "" : "size='$info[size]' ";
1221 $html .= "<input id='$propname' name='$propname' ".
1223 "value='".htmlentities($value, ENT_QUOTES, 'utf-8').
1228 $html .= "<textarea id='$propname' name='$propname' ".
1229 "rows='$info[rows]' cols='$info[cols]' class='code'>".
1230 htmlentities($value, ENT_QUOTES, 'utf-8').
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').
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').
1251 if (isset($allowed[$propname])) {
1252 $html .= mtrack_select_box($propname,
1253 $allowed[$propname], $value);
1255 $html .= mtrack_select_box($propname,
1256 $info['options'], $value);
1261 if (isset($allowed[$propname])) {
1262 $html .= mtrack_multi_select_box($propname,
1263 $info['label'] . " (select to add)",
1264 $allowed[$propname], $value);
1266 $html .= mtrack_multi_select_box($propname,
1267 $info['label'] . " (select to add)",
1268 $info['options'], $value);
1275 if ($info['ownrow']) {
1280 $html .= "</table>\n";
1282 if (is_string($fsid)) {
1283 $html .= "</fieldset>\n";
1287 $html .= "<fieldset id='action-container'><legend>Action</legend>\n";
1289 // FIXME: workflow inspired actions listed here
1290 if (!isset($_POST['action'])) {
1291 $_POST['action'] = 'none';
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";
1298 if ($issue->status != 'closed') {
1299 $html .= mtrack_radio('action', 'accept', $_POST['action']);
1300 $html .= " <label for='accept'>Accept ticket</label><br>\n";
1302 $ST = new MTrackTicketState;
1303 $ST = $ST->enumerate();
1304 unset($ST['closed']);
1305 unset($ST[$status]);
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);
1313 $html .= mtrack_radio('action', 'fixed', $_POST['action']);
1314 $html .= " <label for='fixed'>Resolve as fixed</label><br>\n";
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);
1325 $html .= mtrack_radio('action', 'reopen', $_POST['action']);
1326 $html .= " <label for='reopen'>Reopen ticket</label><br>\n";
1331 $spent = empty($_POST['spent']) ? '' : htmlentities($_POST['spent'], ENT_QUOTES, 'utf-8');
1332 if (!strlen($spent)) {
1335 $html .= "<label for='spent'>Log time spent (hours)</label> ";
1336 $html .= "<input type='text' name='spent' value='$spent'><br>\n";
1339 $html .= MTrackAttachment::renderDeleteList("ticket:$issue->tid");
1342 <label for='attachments[]'>Select file(s) to be attached</label>
1343 <input type='file' class='multi' name='attachments[]'>
1346 $html .= "</fieldset>";
1347 $html .= "<fieldset id='comment-container'><legend>Comment</legend>\n";
1350 <textarea name='comment' id="comment"
1351 class="wiki shortwiki" rows="5" cols="78">
1353 if (isset($_POST['comment'])) {
1354 $html .= htmlentities($_POST['comment'], ENT_QUOTES, 'utf-8');
1356 $html .= "</textarea>";
1358 $html .= "</fieldset>";
1359 $html .= MTrackCaptcha::emit('ticket');
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>
1370 <button class='mtrack-make-comment'>Add Comment</button>