1 --- eventum-2.2/htdocs/ajax/order.php 1970-01-01 02:00:00.000000000 +0200
2 +++ eventum-2.2-order/htdocs/ajax/order.php 2009-10-12 22:10:36.429185594 +0300
5 +require_once(dirname(__FILE__) . '/../init.php');
6 +require_once(APP_INC_PATH . "class.auth.php");
7 +require_once(APP_INC_PATH . "class.issue.php");
10 +if (!Auth::hasValidCookie(APP_COOKIE)) {
16 +if (!isset($_POST['before']) || !isset($_POST['after'])) {
20 +parse_str($_POST['before'], $before);
21 +parse_str($_POST['after'], $after);
23 +$before = $before['issue_list_table'];
24 +$after = $after['issue_list_table'];
27 +$before = array_slice($before, 2, count($before)-3);
28 +$after = array_slice($after, 2, count($after)-3);
30 +if (count($before) != count($after) or count($before) < 1) {
34 +$usr_id = Auth::getUserID();
36 +$order = Issue::getIssueOrderByUser($usr_id);
38 +if (!count($order)) {
39 + // no prev order list
43 +$after_filterd = array();
44 +$before_filterd = array();
46 +// remove issues that are not assigned to me
47 +foreach ($after as $id) {
48 + if (isset($order[$id])) {
49 + $after_filterd[] = $id;
52 +foreach ($before as $id) {
53 + if (isset($order[$id])) {
54 + $before_filterd[] = $id;
58 +foreach ($after_filterd as $key => $nID) {
59 + if ($nID != $before_filterd[$key]) {
62 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
64 + isu_order = " . $order[$before_filterd[$key]] . "
66 + isu_iss_id = $nID AND
67 + isu_usr_id = $usr_id";
68 + $res = DB_Helper::getInstance()->query($stmt);
69 + if (PEAR::isError($res)) {
70 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
71 + die('update failed');
76 --- eventum-2.2/htdocs/ajax/update.php 1970-01-01 02:00:00.000000000 +0200
77 +++ eventum-2.2-order/htdocs/ajax/update.php 2009-10-12 22:10:36.439185157 +0300
80 +require_once(dirname(__FILE__) . '/../init.php');
81 +require_once(APP_INC_PATH . "class.auth.php");
82 +require_once(APP_INC_PATH . "class.issue.php");
85 +if (!Auth::hasValidCookie(APP_COOKIE)) {
89 +if (!is_numeric($_POST['issueID'])) {
93 +switch ($_POST['fieldName']) {
94 + case 'expected_resolution_date':
95 + $day = (int)$_POST['day'];
96 + $month = (int)$_POST['month'];
97 + $year = (int)$_POST['year'];
98 + if ($day == 0 && $month == 1 && $year == 0) {
102 + $date = sprintf('%04d-%02d-%02d', $year, $month, $day);
104 + if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], $date) !== -1) {
105 + if (!is_null($date)) {
106 + echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
109 + echo 'update failed';
114 + die('object type not supported');
117 --- eventum-2.2/htdocs/css/style.css 2009-09-14 18:07:55.000000000 +0300
118 +++ eventum-2.2-order/htdocs/css/style.css 2009-10-12 22:10:36.439185157 +0300
121 font-family: Verdana, Arial, Helvetica, sans-serif;
124 \ No newline at end of file
126 +.tDnD_whileDrag td {
127 + background-color: #ffffdd;
129 +.tDnD_whileDrag td {
130 + border: 1px solid red;
140 + background-image: url(../images/updown2.gif);
141 + background-repeat: no-repeat;
142 + background-position: center center;
144 --- eventum-2.2/htdocs/js/global.js 2009-09-14 18:07:55.000000000 +0300
145 +++ eventum-2.2-order/htdocs/js/global.js 2009-10-12 22:10:36.439185157 +0300
147 firstDay: user_prefs.week_firstday
151 +$(document).ready(function() {
152 + // dialog type calender isn't working in Konqueror beacuse it's not a supported browser for either jQuery or jQuery UI
153 + // http://groups.google.com/group/jquery-ui/browse_thread/thread/ea61238c34cb5f33/046837b02fb90b5c
154 + if (navigator.appName != 'Konqueror') {
155 + $(".inline_date_pick").click(function() {
156 + var masterObj = this;
157 + var masterObjPos = $(masterObj).offset();
158 + // offset gives uses top and left but datepicker needs pageX and pageY
159 + var masterObjPos = {pageX: masterObjPos.left, pageY: masterObjPos.top};
160 + $(this).datepicker(
161 + // we use dialog type calender so we won't haveto have a hidden element on the page
164 + masterObj.innerHTML,
166 + function (date, dteObj) {
167 + fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|'));
168 + issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1);
171 + dteObj.selectedDay = 0;
172 + dteObj.selectedMonth = 0;
173 + dteObj.selectedYear = 0;
175 + //alertProperties(date);
176 + $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) {
177 + masterObj.innerHTML = data;
181 + {dateFormat: 'dd M yy', duration: ""},
182 + // position of the datepicker calender - taken from div's offset
191 --- eventum-2.2/htdocs/js/global.js.~1~ 1970-01-01 02:00:00.000000000 +0200
192 +++ eventum-2.2-order/htdocs/js/global.js.~1~ 2009-09-14 18:07:55.000000000 +0300
196 +var today = new Date();
197 +var expires = new Date(today.getTime() + (56 * 86400000));
199 +function addFileRow(element_name, field_name)
201 + if (document.all) {
202 + var fileTable = document.all[element_name];
203 + } else if (!document.all && document.getElementById) {
204 + var fileTable = document.getElementById(element_name);
209 + rows = fileTable.rows.length;
211 + // check if last box is empty and if it is, don't add another
212 + if (document.all) {
213 + var last_field = document.all[field_name + '_' + rows];
214 + } else if (!document.all && document.getElementById) {
215 + var last_field = document.getElementById(field_name + '_' + rows);
217 + if (last_field && last_field.value == '') {
221 + newRow = fileTable.insertRow(rows);
222 + cell = newRow.insertCell(0);
223 + if (document.all) {
224 + cell.innerHTML = '<input id="' + field_name + '_' + (rows+1) + '" class="shortcut" size="40" type="file" name="' + field_name + '" onChange="javascript:addFileRow(\'' + element_name + '\', \'' + field_name + '\');">';
226 + var input = document.createElement('INPUT');
227 + input.setAttribute('type', 'file');
228 + input.name = field_name;
229 + input.className = 'shortcut';
231 + input.onchange = new Function('addFileRow(\'' + element_name + '\', \'' + field_name + '\');');
232 + input.id = field_name + '_' + (rows+1);
233 + cell.appendChild(input);
237 +function inArray(value, stack)
239 + for (var i = 0; i < stack.length; i++) {
240 + if (stack[i] == value) {
247 +function getEmailFromAddress(str)
249 + var first_pos = str.lastIndexOf('<');
250 + var second_pos = str.lastIndexOf('>');
251 + if ((first_pos != -1) && (second_pos != -1)) {
252 + return str.substring(first_pos+1, second_pos);
258 +function closeAndRefresh()
260 + opener.location.href = opener.location;
264 +function str_replace(s, srch, rplc)
267 + var tmp_before = new String();
268 + var tmp_after = new String();
269 + var tmp_output = new String();
270 + var int_before = 0;
273 + while (tmp.toUpperCase().indexOf(srch.toUpperCase()) > -1) {
274 + int_before = tmp.toUpperCase().indexOf(srch.toUpperCase());
275 + tmp_before = tmp.substring(0, int_before);
276 + tmp_output = tmp_output + tmp_before;
277 + tmp_output = tmp_output + rplc;
278 + int_after = tmp.length - srch.length + 1;
279 + tmp = tmp.substring(int_before + srch.length);
282 + return tmp_output + tmp;
285 +function displayFixedWidth(element_name)
287 + var el = getPageElement(element_name);
288 + // only do this for mozilla
290 + var content = el.innerHTML;
291 + el.innerHTML = '<pre>' + str_replace(content, "<br>", '') + '</pre>';
294 + el.style.fontFamily = 'Mono, Monaco, Courier New, Courier';
295 + el.style.whiteSpace = 'pre';
298 +function showSelections(form_name, field_name)
300 + var f = getForm(form_name);
301 + var field = getFormElement(f, field_name);
302 + var selections = getSelectedItems(field);
303 + var selected_names = new Array();
304 + for (var i = 0; i < selections.length; i++) {
305 + selected_names.push(selections[i].text);
307 + var display_div = getPageElement('selection_' + field_name);
308 + display_div.innerHTML = 'Current Selections: ' + selected_names.join(', ');
311 +function replaceWords(str, original, replacement)
313 + var lines = str.split("\n");
314 + for (var i = 0; i < lines.length; i++) {
315 + lines[i] = replaceWordsOnLine(lines[i], original, replacement);
317 + return lines.join("\n");
320 +function replaceWordsOnLine(str, original, replacement)
322 + var words = str.split(' ');
323 + for (var i = 0; i < words.length; i++) {
324 + words[i] = words[i].replace(/^\s*/, '').replace(/\s*$/, '');
325 + if (words[i] == original) {
326 + words[i] = replacement;
329 + return words.join(' ');
332 +function checkSpelling(form_name, field_name)
334 + var features = 'width=420,height=400,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
335 + var popupWin = window.open('spell_check.php?form_name=' + form_name + '&field_name=' + field_name, '_spellChecking', features);
339 +function updateTimeFields(form_name, year_field, month_field, day_field, hour_field, minute_field, date)
341 + var f = getForm(form_name);
342 + if (typeof date == 'undefined') {
345 + selectOption(f, month_field, padDateValue(date.getMonth()+1));
346 + selectOption(f, day_field, padDateValue(date.getDate()));
347 + selectOption(f, year_field, date.getFullYear());
348 + selectOption(f, hour_field, padDateValue(date.getHours()));
349 + // minutes need special case due the 5 minute granularity
350 + var minutes = Math.floor(date.getMinutes() / 5) * 5;
351 + selectOption(f, minute_field, padDateValue(minutes));
354 +function padDateValue(str)
356 + if (str.length == 1) {
362 +function resizeTextarea(page_name, form_name, field_name, change)
364 + var f = getForm(form_name);
365 + var field = getFormElement(f, field_name);
366 + field.cols = field.cols + change;
367 + var cookie_name = 'textarea_' + page_name + '_' + field_name;
368 + setCookie(cookie_name, field.cols, expires);
371 +function removeOptionByValue(f, field_name, value)
373 + var field = getFormElement(f, field_name);
374 + for (var i = 0; i < field.options.length; i++) {
375 + if (field.options[i].value == value) {
376 + field.options[i] = null;
381 +function getTotalCheckboxes(f, field_name)
384 + for (var i = 0; i < f.elements.length; i++) {
385 + if (f.elements[i].name == field_name) {
392 +function getTotalCheckboxesChecked(f, field_name)
395 + for (var i = 0; i < f.elements.length; i++) {
396 + if ((f.elements[i].name == field_name) && (f.elements[i].checked)) {
403 +function hideComboBoxes(except_field)
405 + for (var i = 0; i < document.forms.length; i++) {
406 + for (var y = 0; y < document.forms[i].elements.length; y++) {
407 + if (((document.forms[i].elements[y].type == 'select-one') ||
408 + (document.forms[i].elements[y].type == 'select-multiple')) &&
409 + (document.forms[i].elements[y].name != except_field) &&
410 + (document.forms[i].elements[y].name != 'lookup') &&
411 + (document.forms[i].elements[y].name != 'lookup[]')) {
412 + document.forms[i].elements[y].style.visibility = 'hidden';
418 +function showComboBoxes()
420 + for (var i = 0; i < document.forms.length; i++) {
421 + for (var y = 0; y < document.forms[i].elements.length; y++) {
422 + if (((document.forms[i].elements[y].type == 'select-one') ||
423 + (document.forms[i].elements[y].type == 'select-multiple')) &&
424 + (document.forms[i].elements[y].name != 'lookup') &&
425 + (document.forms[i].elements[y].name != 'lookup[]')) {
426 + document.forms[i].elements[y].style.visibility = 'visible';
432 +function getOverlibContents(options, target_form, target_field, is_multiple)
434 + hideComboBoxes(target_field);
435 + var html = '<form name="overlib_form" onSubmit="javascript:return lookupOption(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<br /><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup';
436 + if ((is_multiple != null) && (is_multiple == true)) {
439 + html += '\');"><input class="button_overlib" type="submit" value="Lookup"><br />'
440 + + '<input type="text" name="id_number" size="24" class="lookup_field_overlib" value="id #" onFocus="javascript:this.value=\'\';">'
441 + + '<input type="button" class="button_overlib" value="Add By ID" onClick="lookupByID(document.forms[\'overlib_form\'].id_number, \'' + target_form + '\', \'' + target_field + '\')"></form>';
445 +function getFillInput(options, target_form, target_field)
447 + hideComboBoxes(target_field);
448 + return '<form onSubmit="javascript:return fillInput(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<input class="button_overlib" type="submit" value="Lookup"><br><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup\');"></form>';
451 +function lookupOption(f, target_form, target_field)
454 + for (var i = 0; i < w.forms.length; i++) {
455 + if (w.forms[i].name == target_form) {
456 + var test = getFormElement(f, 'lookup');
458 + var field = getFormElement(f, 'lookup[]');
459 + var target = getFormElement(getForm(target_form), target_field);
460 + clearSelectedOptions(target);
461 + selectOptions(w.forms[i], target_field, getSelectedItems(field));
463 + options = getSelectedOption(f, 'lookup');
464 + if (options == -1) {
467 + selectOption(w.forms[i], target_field, options);
477 +function lookupByID(field, target_form, target_field)
479 + if (!isNumberOnly(field.value)) {
480 + alert('Please enter numbers only');
482 + // try to find value in targer field.
483 + target_obj = document.forms[target_form].elements[target_field];
485 + for (i = 0;i<target_obj.options.length; i++) {
486 + if (target_obj.options[i].value == field.value) {
488 + target_obj.options[i].selected = true;
491 + if (found == false) {
492 + alert('ID #' + field.value + ' was not found');
495 + // check if we should call "showSelection"
496 + if (document.getElementById('selection_' + target_field) != null) {
497 + showSelections(target_form, target_field)
504 +function fillInput(f, target_form, target_field)
506 + var exists = getFormElement(f, 'lookup');
507 + var target_f = getForm(target_form);
509 + var field = getFormElement(f, 'lookup[]');
510 + var target_field = getFormElement(target_f, target_field);
511 + target_field.value = '';
512 + var values = getValues(getSelectedItems(field));
513 + target_field.value = values.join('; ');
514 + errorDetails(target_f, target_field, false);
516 + var field_value = getSelectedOption(f, 'lookup');
517 + var field = getFormElement(target_f, target_field);
518 + field.value = field_value;
519 + errorDetails(target_f, target_field, false);
526 +function lookupField(f, search_field, field_name, callbacks)
528 + var search = search_field.value;
529 + if (isWhitespace(search)) {
532 + var target_field = getFormElement(f, field_name);
533 + if (!target_field) {
534 + target_field = getFormElement(f, field_name + '[]');
536 + for (var i = 0; i < target_field.options.length; i++) {
537 + var value = target_field.options[i].text.toUpperCase();
538 + if (target_field.type == 'select-multiple') {
539 + // if we are targetting a multiple select box, then unselect everything
540 + // before selecting the matched option
541 + if (startsWith(value, search.toUpperCase())) {
542 + clearSelectedOptions(target_field);
543 + target_field.options[i].selected = true;
544 + // handle calling any callbacks
545 + if (callbacks != null) {
546 + for (var y = 0; y < callbacks.length; y++) {
547 + eval(callbacks[y] + ';');
553 + // normal drop-down boxes will search across the option value, and
554 + // not just the beginning of it (e.g. '*hello*' instead of 'hello*')
555 + if (value.indexOf(search.toUpperCase()) != -1) {
556 + target_field.options[i].selected = true;
557 + // handle calling any callbacks
558 + if (callbacks != null) {
559 + for (var y = 0; y < callbacks.length; y++) {
560 + eval(callbacks[y] + ';');
567 + target_field.selectedIndex = 0;
570 +function clearSelectedOptions(field)
572 + for (var i = 0; i < field.options.length; i++) {
573 + field.options[i].selected = false;
577 +function selectAllOptions(f, field_name)
579 + var field = getFormElement(f, field_name);
580 + for (var y = 0; y < field.options.length; y++) {
581 + field.options[y].selected = true;
585 +function selectOptions(f, field_name, values)
587 + var field = getFormElement(f, field_name);
588 + for (var i = 0; i < values.length; i++) {
589 + for (var y = 0; y < field.options.length; y++) {
590 + if (field.options[y].value == values[i].value) {
591 + field.options[y].selected = true;
597 +function selectOption(f, field_name, value)
599 + field = getFormElement(f, field_name);
600 + for (var i = 0; i < field.options.length; i++) {
601 + if (field.options[i].value == value) {
602 + field.options[i].selected = true;
608 +function setHiddenFieldValue(f, field_name, value)
610 + var field = getFormElement(f, field_name);
611 + field.value = value;
614 +function getForm(form_name)
616 + for (var i = 0; i < document.forms.length; i++) {
617 + if (document.forms[i].name == form_name) {
618 + return document.forms[i];
623 +function getPageElement(id)
625 + if (document.getElementById) {
626 + return document.getElementById(id);
627 + } else if (document.all) {
628 + return document.all[id];
632 +function getOpenerPageElement(name)
634 + if (window.opener.document.getElementById) {
635 + return window.opener.document.getElementById(name);
636 + } else if (window.opener.document.all) {
637 + return window.opener.document.all[name];
641 +function getFormElement(f, field_name, num)
643 + var elements = document.getElementsByName(field_name);
645 + for (var i = 0; i < elements.length; i++) {
646 + if (f != elements[i].form) {
651 + return elements[i];
655 + return elements[i];
661 +function getSelectedItems(field)
663 + var selected = new Array();
664 + for (var i = 0; i < field.options.length; i++) {
665 + if (field.options[i].selected) {
666 + selected[selected.length] = field.options[i];
672 +function getSelectedOptionValues(f, field_name)
674 + var field = getFormElement(f, field_name);
675 + var selected = new Array();
676 + for (var i = 0; i < field.options.length; i++) {
677 + if (field.options[i].selected) {
678 + selected[selected.length] = field.options[i].value;
684 +function removeAllOptions(f, field_name)
686 + var field = getFormElement(f, field_name);
687 + if (field.options.length > 0) {
688 + field.options[0] = null;
689 + removeAllOptions(f, field_name);
693 +function getValues(list)
695 + var values = new Array();
696 + for (var i = 0; i < list.length; i++) {
697 + values[values.length] = list[i].value;
702 +function optionExists(field, option)
704 + for (var i = 0; i < field.options.length; i++) {
705 + if (field.options[i].text == option.text) {
712 +function addOptions(f, field_name, options)
714 + var field = getFormElement(f, field_name);
715 + for (var i = 0; i < options.length; i++) {
716 + if (!optionExists(field, options[i])) {
717 + field.options.length = field.options.length + 1;
718 + field.options[field.options.length-1].text = options[i].text;
719 + field.options[field.options.length-1].value = options[i].value;
724 +function replaceParam(str, param, new_value)
726 + if (str.indexOf("?") == -1) {
727 + return param + "=" + new_value;
729 + var pieces = str.split("?");
730 + var params = pieces[1].split("&");
731 + var new_params = new Array();
732 + for (var i = 0; i < params.length; i++) {
733 + if (params[i].indexOf(param + "=") == 0) {
734 + params[i] = param + "=" + new_value;
736 + new_params[i] = params[i];
738 + // check if the parameter doesn't exist on the URL
739 + if ((str.indexOf("?" + param + "=") == -1) && (str.indexOf("&" + param + "=") == -1)) {
740 + new_params[new_params.length] = param + "=" + new_value;
742 + return new_params.join("&");
746 +function checkRadio(form_name, field_name, num)
748 + var f = getForm(form_name);
749 + var field = getFormElement(f, field_name, num);
750 + if (!field.disabled) {
751 + field.checked = true;
755 +function toggleCheckbox(form_name, field_name, num)
757 + var f = getForm(form_name);
758 + var checkbox = getFormElement(f, field_name, num);
759 + if (checkbox.disabled) {
762 + if (checkbox.checked) {
763 + checkbox.checked = false;
765 + checkbox.checked = true;
770 +function toggleSelectAll(f, field_name)
772 + for (var i = 0; i < f.elements.length; i++) {
773 + if (f.elements[i].disabled) {
776 + if (f.elements[i].name == field_name) {
777 + if (toggle == 'off') {
778 + f.elements[i].checked = true;
780 + f.elements[i].checked = false;
784 + if (toggle == 'off') {
791 +function getCookies()
793 + var t = new Array();
794 + var pieces = new Array();
795 + var cookies = new Object();
796 + if (document.cookie) {
797 + t = document.cookie.split(';');
798 + for (var i = 0; i < t.length; i++) {
799 + pieces = t[i].split('=');
800 + eval('cookies.' + pieces[0].replace('[', '_').replace(']', '_') + ' = "' + pieces[1] + '";');
806 +function isElementVisible(element)
808 + if ((!element.style.display) || (element.style.display == getDisplayStyle())) {
815 +function toggleVisibility(title, create_cookie, use_inline)
817 + var element = getPageElement(title + '1');
818 + if (isElementVisible(element)) {
819 + var new_style = 'none';
821 + var new_style = getDisplayStyle(use_inline);
825 + element = getPageElement(title + i);
829 + element.style.display = new_style;
832 + // if any elements were found, then...
834 + var link_element = getPageElement(title + '_link');
835 + if (link_element) {
836 + if (new_style == 'none') {
837 + link_element.innerHTML = 'show';
838 + link_element.title = 'show details about this section';
840 + link_element.innerHTML = 'hide';
841 + link_element.title = 'hide details about this section';
845 + if (((create_cookie == null) || (create_cookie == false)) && (create_cookie != undefined)) {
848 + setCookie('visibility_' + title, new_style, expires);
852 +function changeVisibility(title, visibility, use_inline)
854 + var element = getPageElement(title);
856 + var new_style = getDisplayStyle(use_inline);
858 + var new_style = 'none';
860 + element.style.display = new_style;
863 +function getDisplayStyle(use_inline)
865 + // kind of hackish, but it works perfectly with IE6 and Mozilla 1.1
867 + if (use_inline == true) {
877 +function getCookie(name)
879 + var start = document.cookie.indexOf(name+"=");
880 + var len = start+name.length+1;
881 + if ((!start) && (name != document.cookie.substring(0,name.length))) return null;
882 + if (start == -1) return null;
883 + var end = document.cookie.indexOf(";",len);
884 + if (end == -1) end = document.cookie.length;
885 + return unescape(document.cookie.substring(len,end));
888 +function setCookie(name, value, expires, path, domain, secure)
890 + document.cookie = name + "=" +escape(value) +
891 + ( (expires) ? ";expires=" + expires.toGMTString() : "") +
892 + ( (path) ? ";path=" + path : "") +
893 + ( (domain) ? ";domain=" + domain : "") +
894 + ( (secure) ? ";secure" : "");
897 +function openHelp(rel_url, topic)
903 + var location = 'top=' + h_offset + ',left=' + w_offset + ',';
904 + if (screen.width) {
905 + location = 'top=' + h_offset + ',left=' + (screen.width - (width + w_offset)) + ',';
907 + var features = 'width=' + width + ',height=' + height + ',' + location + 'resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
908 + var helpWin = window.open(rel_url + 'help.php?topic=' + topic, '_help', features);
912 +function selectOnlyValidOption(selectObj)
914 + if (selectObj.selectedIndex < 1) {
915 + if (selectObj.length == 1) {
916 + selectObj.selectedIndex = 0;
919 + if (selectObj.length <= 2 && selectObj.options[0].value == -1) {
920 + selectObj.selectedIndex = 1;
926 +// this method will confirm that you want the window to close
927 +var checkClose = false;
928 +var closeConfirmMessage = 'Do you want to close this window?';
929 +function handleClose()
931 + if (checkClose == true) {
932 + return closeConfirmMessage;
938 +function checkWindowClose(msg)
940 + if (msg == false) {
941 + checkClose = false;
944 + closeConfirmMessage = msg;
948 +// Replace special characters MS uses for quotes with normal versions
949 +function replaceSpecialCharacters(e)
951 + var s = new String(e.value);
952 + var newString = '';
955 + for (i = 0; i < s.length; i++) {
956 + thisChar = s.charAt(i);
957 + charCode = s.charCodeAt(i);
958 + if ((charCode == 8220) || (charCode == 8221)) {
960 + } else if (charCode == 8217) {
962 + } else if (charCode == 8230) {
964 + } else if (charCode == 8226) {
966 + } else if (charCode == 8211) {
969 + newString = newString + thisChar;
971 + e.value = newString;
975 +function getEventTarget(e)
978 + if (!e) var e = window.event;
979 + if (e.target) targ = e.target;
980 + else if (e.srcElement) targ = e.srcElement;
981 + if (targ.nodeType == 3) // defeat Safari bug
982 + targ = targ.parentNode;
987 +// call when document ready
988 +$(document).ready(function() {
989 + $('.date_picker').datepicker({
990 + dateFormat: 'yy-mm-dd',
991 + firstDay: user_prefs.week_firstday
995 --- eventum-2.2/htdocs/js/jquery/jquery.tablednd.js 1970-01-01 02:00:00.000000000 +0200
996 +++ eventum-2.2-order/htdocs/js/jquery/jquery.tablednd.js 2009-10-12 22:10:36.435851675 +0300
999 + * TableDnD plug-in for JQuery, allows you to drag and drop table rows
1000 + * You can set up various options to control how the system will work
1001 + * Copyright (c) Denis Howlett <denish@isocra.com>
1002 + * Licensed like jQuery, see http://docs.jquery.com/License.
1004 + * Configuration options:
1007 + * This is the style that is assigned to the row during drag. There are limitations to the styles that can be
1008 + * associated with a row (such as you can't assign a border--well you can, but it won't be
1009 + * displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
1010 + * a map (as used in the jQuery css(...) function).
1012 + * This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
1013 + * to what you can do. Also this replaces the original style, so again consider using onDragClass which
1014 + * is simply added and then removed on drop.
1016 + * This class is added for the duration of the drag and then removed when the row is dropped. It is more
1017 + * flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
1018 + * is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
1021 + * Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
1022 + * and the row that was dropped. You can work out the new order of the rows by using
1025 + * Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
1026 + * table and the row which the user has started to drag.
1028 + * Pass a function that will be called as a row is over another row. If the function returns true, allow
1029 + * dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
1030 + * the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
1032 + * This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
1033 + * window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
1036 + * This is the name of a class that you assign to one or more cells in each row that is draggable. If you
1037 + * specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
1038 + * will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
1039 + * the whole row is draggable.
1041 + * Other ways to control behaviour:
1043 + * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
1044 + * that you don't want to be draggable.
1046 + * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
1047 + * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
1048 + * an ID as must all the rows.
1052 + * $("...").tableDnDUpdate()
1053 + * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
1054 + * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
1055 + * The table maintains the original configuration (so you don't have to specify it again).
1057 + * $("...").tableDnDSerialize()
1058 + * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
1059 + * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
1062 + * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
1064 + * Version 0.2: 2008-02-20 First public version
1065 + * Version 0.3: 2008-02-07 Added onDragStart option
1066 + * Made the scroll amount configurable (default is 5 as before)
1067 + * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
1068 + * Added onAllowDrop to control dropping
1069 + * Fixed a bug which meant that you couldn't set the scroll amount in both directions
1070 + * Added serialize method
1071 + * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
1073 + * Improved the serialize method to use a default (and settable) regular expression.
1074 + * Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
1076 +jQuery.tableDnD = {
1077 + /** Keep hold of the current table being dragged */
1078 + currentTable : null,
1079 + /** Keep hold of the current drag object if any */
1081 + /** The current mouse offset */
1082 + mouseOffset: null,
1083 + /** Remember the old value of Y so that we don't do too much processing */
1086 + /** Actually build the structure */
1087 + build: function(options) {
1088 + // Set up the defaults if any
1090 + this.each(function() {
1091 + // This is bound to each matching table, set up the defaults and override with user options
1092 + this.tableDnDConfig = jQuery.extend({
1093 + onDragStyle: null,
1094 + onDropStyle: null,
1095 + // Add in the default class for whileDragging
1096 + onDragClass: "tDnD_whileDrag",
1098 + onDragStart: null,
1100 + serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
1101 + serializeParamName: null, // If you want to specify another parameter name instead of the table ID
1102 + dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
1103 + }, options || {});
1104 + // Now make the rows draggable
1105 + jQuery.tableDnD.makeDraggable(this);
1108 + // Now we need to capture the mouse up and mouse move event
1109 + // We can use bind so that we don't interfere with other event handlers
1111 + .bind('mousemove', jQuery.tableDnD.mousemove)
1112 + .bind('mouseup', jQuery.tableDnD.mouseup);
1114 + // Don't break the chain
1118 + /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
1119 + makeDraggable: function(table) {
1120 + var config = table.tableDnDConfig;
1121 + if (table.tableDnDConfig.dragHandle) {
1122 + // We only need to add the event to the specified cells
1123 + var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
1124 + cells.each(function() {
1125 + // The cell is bound to "this"
1126 + jQuery(this).mousedown(function(ev) {
1127 + jQuery.tableDnD.dragObject = this.parentNode;
1128 + jQuery.tableDnD.currentTable = table;
1129 + jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
1130 + if (config.onDragStart) {
1131 + // Call the onDrop method if there is one
1132 + config.onDragStart(table, this);
1138 + // For backwards compatibility, we add the event to the whole row
1139 + var rows = jQuery("tr", table); // get all the rows as a wrapped set
1140 + rows.each(function() {
1141 + // Iterate through each row, the row is bound to "this"
1142 + var row = jQuery(this);
1143 + if (! row.hasClass("nodrag")) {
1144 + row.mousedown(function(ev) {
1145 + if (ev.target.tagName == "TD") {
1146 + jQuery.tableDnD.dragObject = this;
1147 + jQuery.tableDnD.currentTable = table;
1148 + jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
1149 + if (config.onDragStart) {
1150 + // Call the onDrop method if there is one
1151 + config.onDragStart(table, this);
1155 + }).css("cursor", "move"); // Store the tableDnD object
1161 + updateTables: function() {
1162 + this.each(function() {
1163 + // this is now bound to each matching table
1164 + if (this.tableDnDConfig) {
1165 + jQuery.tableDnD.makeDraggable(this);
1170 + /** Get the mouse coordinates from the event (allowing for browser differences) */
1171 + mouseCoords: function(ev){
1172 + if(ev.pageX || ev.pageY){
1173 + return {x:ev.pageX, y:ev.pageY};
1176 + x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
1177 + y:ev.clientY + document.body.scrollTop - document.body.clientTop
1181 + /** Given a target element and a mouse event, get the mouse offset from that element.
1182 + To do this we need the element's position and the mouse position */
1183 + getMouseOffset: function(target, ev) {
1184 + ev = ev || window.event;
1186 + var docPos = this.getPosition(target);
1187 + var mousePos = this.mouseCoords(ev);
1188 + return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
1191 + /** Get the position of an element by going up the DOM tree and adding up all the offsets */
1192 + getPosition: function(e){
1195 + /** Safari fix -- thanks to Luis Chato for this! */
1196 + if (e.offsetHeight == 0) {
1197 + /** Safari 2 doesn't correctly grab the offsetTop of a table row
1198 + this is detailed here:
1199 + http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
1200 + the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
1201 + note that firefox will return a text node as a first child, so designing a more thorough
1202 + solution may need to take that into account, for now this seems to work in firefox, safari, ie */
1203 + e = e.firstChild; // a table cell
1206 + while (e.offsetParent){
1207 + left += e.offsetLeft;
1208 + top += e.offsetTop;
1209 + e = e.offsetParent;
1212 + left += e.offsetLeft;
1213 + top += e.offsetTop;
1215 + return {x:left, y:top};
1218 + mousemove: function(ev) {
1219 + if (jQuery.tableDnD.dragObject == null) {
1223 + var dragObj = jQuery(jQuery.tableDnD.dragObject);
1224 + var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1225 + var mousePos = jQuery.tableDnD.mouseCoords(ev);
1226 + var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
1227 + //auto scroll the window
1228 + var yOffset = window.pageYOffset;
1229 + if (document.all) {
1230 + // Windows version
1231 + //yOffset=document.body.scrollTop;
1232 + if (typeof document.compatMode != 'undefined' &&
1233 + document.compatMode != 'BackCompat') {
1234 + yOffset = document.documentElement.scrollTop;
1236 + else if (typeof document.body != 'undefined') {
1237 + yOffset=document.body.scrollTop;
1242 + if (mousePos.y-yOffset < config.scrollAmount) {
1243 + window.scrollBy(0, -config.scrollAmount);
1245 + var windowHeight = window.innerHeight ? window.innerHeight
1246 + : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
1247 + if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
1248 + window.scrollBy(0, config.scrollAmount);
1253 + if (y != jQuery.tableDnD.oldY) {
1254 + // work out if we're going up or down...
1255 + var movingDown = y > jQuery.tableDnD.oldY;
1256 + // update the old value
1257 + jQuery.tableDnD.oldY = y;
1258 + // update the style to show we're dragging
1259 + if (config.onDragClass) {
1260 + dragObj.addClass(config.onDragClass);
1262 + dragObj.css(config.onDragStyle);
1264 + // If we're over a row then move the dragged row to there so that the user sees the
1265 + // effect dynamically
1266 + var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
1268 + // TODO worry about what happens when there are multiple TBODIES
1269 + if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
1270 + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
1271 + } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
1272 + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
1280 + /** We're only worried about the y position really, because we can only move rows up and down */
1281 + findDropTargetRow: function(draggedRow, y) {
1282 + var rows = jQuery.tableDnD.currentTable.rows;
1283 + for (var i=0; i<rows.length; i++) {
1284 + var row = rows[i];
1285 + var rowY = this.getPosition(row).y;
1286 + var rowHeight = parseInt(row.offsetHeight)/2;
1287 + if (row.offsetHeight == 0) {
1288 + rowY = this.getPosition(row.firstChild).y;
1289 + rowHeight = parseInt(row.firstChild.offsetHeight)/2;
1291 + // Because we always have to insert before, we need to offset the height a bit
1292 + if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
1293 + // that's the row we're over
1294 + // If it's the same as the current row, ignore it
1295 + if (row == draggedRow) {return null;}
1296 + var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1297 + if (config.onAllowDrop) {
1298 + if (config.onAllowDrop(draggedRow, row)) {
1304 + // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
1305 + var nodrop = jQuery(row).hasClass("nodrop");
1318 + mouseup: function(e) {
1319 + if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
1320 + var droppedRow = jQuery.tableDnD.dragObject;
1321 + var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1322 + // If we have a dragObject, then we need to release it,
1323 + // The row will already have been moved to the right place so we just reset stuff
1324 + if (config.onDragClass) {
1325 + jQuery(droppedRow).removeClass(config.onDragClass);
1327 + jQuery(droppedRow).css(config.onDropStyle);
1329 + jQuery.tableDnD.dragObject = null;
1330 + if (config.onDrop) {
1331 + // Call the onDrop method if there is one
1332 + config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
1334 + jQuery.tableDnD.currentTable = null; // let go of the table too
1338 + serialize: function() {
1339 + if (jQuery.tableDnD.currentTable) {
1340 + return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
1342 + return "Error: No Table id set, you need to set an id on your table and every row";
1346 + serializeTable: function(table) {
1348 + var tableId = table.id;
1349 + var rows = table.rows;
1350 + for (var i=0; i<rows.length; i++) {
1351 + if (result.length > 0) result += "&";
1352 + var rowId = rows[i].id;
1353 + if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
1354 + rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
1357 + result += tableId + '[]=' + rowId;
1362 + serializeTables: function() {
1364 + this.each(function() {
1365 + // this is now bound to each matching table
1366 + result += jQuery.tableDnD.serializeTable(this);
1375 + tableDnD : jQuery.tableDnD.build,
1376 + tableDnDUpdate : jQuery.tableDnD.updateTables,
1377 + tableDnDSerialize: jQuery.tableDnD.serializeTables
1380 \ No newline at end of file
1381 --- eventum-2.2/htdocs/list.php 2009-09-14 18:07:55.000000000 +0300
1382 +++ eventum-2.2-order/htdocs/list.php 2009-10-12 22:10:36.435851675 +0300
1384 $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
1387 +@$reorder_usr_id = $_REQUEST["reorder_user"];
1388 +@$reorder_issue_id = $_REQUEST["reorder_source"];
1389 +@$reorder_neworder = $_REQUEST["reorder_neworder"];
1390 +Issue::reorderUserIssues($reorder_usr_id, $reorder_issue_id, $reorder_neworder);
1392 $options = Issue::saveSearchParams();
1393 $tpl->assign("options", $options);
1394 $tpl->assign("sorting", Issue::getSortingInfo($options));
1397 $assign_options += $users;
1399 +// get the isu_order (assignated users) ordering user
1400 +if (!empty($options["users"])) {
1401 + if ($options["users"] == -2) {
1402 + $isu_order_user = $usr_id;
1404 + if ($options["users"] > 0) {
1405 + $isu_order_user = $options["users"];
1407 + unset($isu_order_user);
1410 + unset($isu_order_user);
1412 +$tpl->assign("isu_order_user", $isu_order_user);
1414 $list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
1415 $tpl->assign("list", $list["list"]);
1416 $tpl->assign("list_info", $list["info"]);
1417 --- eventum-2.2/htdocs/list.php.~1~ 1970-01-01 02:00:00.000000000 +0200
1418 +++ eventum-2.2-order/htdocs/list.php.~1~ 2009-09-14 18:07:55.000000000 +0300
1421 +/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
1422 +// +----------------------------------------------------------------------+
1423 +// | Eventum - Issue Tracking System |
1424 +// +----------------------------------------------------------------------+
1425 +// | Copyright (c) 2003 - 2008 MySQL AB |
1426 +// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
1428 +// | This program is free software; you can redistribute it and/or modify |
1429 +// | it under the terms of the GNU General Public License as published by |
1430 +// | the Free Software Foundation; either version 2 of the License, or |
1431 +// | (at your option) any later version. |
1433 +// | This program is distributed in the hope that it will be useful, |
1434 +// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
1435 +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1436 +// | GNU General Public License for more details. |
1438 +// | You should have received a copy of the GNU General Public License |
1439 +// | along with this program; if not, write to: |
1441 +// | Free Software Foundation, Inc. |
1442 +// | 59 Temple Place - Suite 330 |
1443 +// | Boston, MA 02111-1307, USA. |
1444 +// +----------------------------------------------------------------------+
1445 +// | Authors: João Prado Maia <jpm@mysql.com> |
1446 +// +----------------------------------------------------------------------+
1450 +require_once dirname(__FILE__) . '/../init.php';
1452 +$tpl = new Template_Helper();
1453 +$tpl->setTemplate("list.tpl.html");
1455 +Auth::checkAuthentication(APP_COOKIE);
1456 +$usr_id = Auth::getUserID();
1457 +$prj_id = Auth::getCurrentProject();
1459 +$pagerRow = Issue::getParam('pagerRow');
1460 +if (empty($pagerRow)) {
1463 +$rows = Issue::getParam('rows');
1464 +if (empty($rows)) {
1465 + $rows = APP_DEFAULT_PAGER_SIZE;
1468 +if (@$_REQUEST['view'] == 'my_assignments') {
1469 + $profile = Search_Profile::getProfile($usr_id, $prj_id, 'issue');
1470 + Search_Profile::remove($usr_id, $prj_id, 'issue');
1471 + Auth::redirect("list.php?users=$usr_id&hide_closed=1&rows=$rows&sort_by=" .
1472 + $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
1475 +$options = Issue::saveSearchParams();
1476 +$tpl->assign("options", $options);
1477 +$tpl->assign("sorting", Issue::getSortingInfo($options));
1479 +// generate options for assign list. If there are groups and user is above a customer, include groups
1480 +$groups = Group::getAssocList($prj_id);
1481 +$users = Project::getUserAssocList($prj_id, 'active', User::getRoleID('Customer'));
1482 +$assign_options = array(
1483 + "" => ev_gettext("Any"),
1484 + "-1" => ev_gettext("un-assigned"),
1485 + "-2" => ev_gettext("myself and un-assigned")
1487 +if (Auth::isAnonUser())
1488 + unset($assign_options["-2"]);
1489 +else if (User::getGroupID($usr_id) != '') {
1490 + $assign_options['-3'] = ev_gettext('myself and my group');
1491 + $assign_options['-4'] = ev_gettext('myself, un-assigned and my group');
1493 +if ((count($groups) > 0) && (Auth::getCurrentRole() > User::getRoleID("Customer"))) {
1494 + foreach ($groups as $grp_id => $grp_name) {
1495 + $assign_options["grp:$grp_id"] = ev_gettext("Group") . ": " . $grp_name;
1498 +$assign_options += $users;
1500 +$list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
1501 +$tpl->assign("list", $list["list"]);
1502 +$tpl->assign("list_info", $list["info"]);
1503 +$tpl->assign("csv_data", base64_encode(@$list["csv"]));
1505 +$tpl->assign("columns", Display_Column::getColumnsToDisplay($prj_id, 'list_issues'));
1506 +$tpl->assign("priorities", Priority::getAssocList($prj_id));
1507 +$tpl->assign("status", Status::getAssocStatusList($prj_id));
1508 +$tpl->assign("assign_options", $assign_options);
1509 +$tpl->assign("custom", Filter::getAssocList($prj_id));
1510 +$tpl->assign("csts", Filter::getListing(true));
1511 +$tpl->assign("active_filters", Filter::getActiveFilters($options));
1512 +$tpl->assign("categories", Category::getAssocList($prj_id));
1513 +$tpl->assign("releases", Release::getAssocList($prj_id, true));
1514 +$tpl->assign("reporters", Project::getReporters($prj_id));
1516 +$prefs = Prefs::get($usr_id);
1517 +$tpl->assign("refresh_rate", $prefs['list_refresh_rate'] * 60);
1518 +$tpl->assign("refresh_page", "list.php");
1520 +// items needed for bulk update tool
1521 +if (Auth::getCurrentRole() > User::getRoleID("Developer")) {
1522 + $tpl->assign("users", $users);
1524 + if (Workflow::hasWorkflowIntegration($prj_id)) {
1525 + $open_statuses = Workflow::getAllowedStatuses($prj_id);
1527 + $open_statuses = Status::getAssocStatusList($prj_id, false);
1530 + $tpl->assign("open_status", $open_statuses);
1531 + $tpl->assign("closed_status", Status::getClosedAssocList($prj_id));
1532 + $tpl->assign("available_releases", Release::getAssocList($prj_id));
1535 +$tpl->displayTemplate();
1536 --- eventum-2.2/lib/eventum/class.display_column.php 2009-09-14 18:07:55.000000000 +0300
1537 +++ eventum-2.2-order/lib/eventum/class.display_column.php 2009-10-12 22:10:36.429185594 +0300
1538 @@ -230,7 +230,10 @@
1540 "iss_expected_resolution_date" => array(
1541 "title" => ev_gettext("Expected Resolution Date")
1544 + "isu_order" => array(
1545 + "title" => ev_gettext("Order")
1549 return $columns[$page];
1550 --- eventum-2.2/lib/eventum/class.display_column.php.~1~ 1970-01-01 02:00:00.000000000 +0200
1551 +++ eventum-2.2-order/lib/eventum/class.display_column.php.~1~ 2009-09-14 18:07:55.000000000 +0300
1554 +/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
1555 +// +----------------------------------------------------------------------+
1556 +// | Eventum - Issue Tracking System |
1557 +// +----------------------------------------------------------------------+
1558 +// | Copyright (c) 2003 - 2008 MySQL AB |
1559 +// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
1561 +// | This program is free software; you can redistribute it and/or modify |
1562 +// | it under the terms of the GNU General Public License as published by |
1563 +// | the Free Software Foundation; either version 2 of the License, or |
1564 +// | (at your option) any later version. |
1566 +// | This program is distributed in the hope that it will be useful, |
1567 +// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
1568 +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1569 +// | GNU General Public License for more details. |
1571 +// | You should have received a copy of the GNU General Public License |
1572 +// | along with this program; if not, write to: |
1574 +// | Free Software Foundation, Inc. |
1575 +// | 59 Temple Place - Suite 330 |
1576 +// | Boston, MA 02111-1307, USA. |
1577 +// +----------------------------------------------------------------------+
1578 +// | Authors: Bryan Alsdorf <bryan@mysql.com> |
1579 +// +----------------------------------------------------------------------+
1585 + * Class to handle determining which columns should be displayed and in what order
1586 + * on a page (e.g. Issue Listing page).
1588 + * @author Bryan Alsdorf <bryan@mysql.com>
1592 +class Display_Column
1595 + * Returns the columns that should be displayed for the specified page.
1596 + * This method will remove columns that should not be displayed, due to
1597 + * lack of customer integration or insufficient role.
1600 + * @param integer $prj_id The ID of the project.
1601 + * @param string $page The page to return columns for.
1602 + * @return array An array of columns that should be displayed.
1604 + function getColumnsToDisplay($prj_id, $page)
1608 + // poor man's caching system
1609 + if (!empty($returns[$prj_id][$page])) {
1610 + return $returns[$prj_id][$page];
1613 + $current_role = Auth::getCurrentRole();
1614 + $data = self::getSelectedColumns($prj_id, $page);
1615 + $has_customer_integration = Customer::hasCustomerIntegration($prj_id);
1616 + $only_with_customers = array('iss_customer_id', 'support_level');
1618 + // remove groups if there are no groups in the system.
1619 + if (count(Group::getAssocList($prj_id)) < 1) {
1620 + unset($data['iss_grp_id']);
1622 + // remove category column if there are no categories in the system
1623 + if (count(Category::getAssocList($prj_id)) < 1) {
1624 + unset($data['prc_title']);
1626 + // remove custom fields column if there are no custom fields
1627 + if (count(Custom_Field::getFieldsToBeListed($prj_id)) < 1) {
1628 + unset($data['custom_fields']);
1630 + // remove customer field if user has a role of customer
1631 + if ($current_role == User::getRoleID("Customer")) {
1632 + unset($data['iss_customer_id']);
1635 + foreach ($data as $field => $info) {
1636 + // remove fields based on role
1637 + if ($info['min_role'] > $current_role) {
1638 + unset($data[$field]);
1641 + // remove fields based on customer integration
1642 + if (!$has_customer_integration && (in_array($field, $only_with_customers))) {
1643 + unset($data[$field]);
1647 + $data[$field] = self::getColumnInfo($page, $field);
1649 + $returns[$prj_id][$page] = $data;
1655 + * Returns the columns that have been selected to be displayed on the specified page. This list
1656 + * contains all selected columns, even if they won't actually be displayed.
1659 + * @param integer $prj_id The ID of the project.
1660 + * @param string $page The page to return columns for.
1661 + * @return array An array of columns that should be displayed.
1663 + function getSelectedColumns($prj_id, $page)
1667 + // poor man's caching system
1668 + if (!empty($returns[$prj_id][$page])) {
1669 + return $returns[$prj_id][$page];
1677 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1679 + ctd_prj_id = $prj_id AND
1680 + ctd_page = '$page'
1683 + $res = DB_Helper::getInstance()->getAssoc($stmt, false, array(), DB_FETCHMODE_ASSOC);
1684 + if (PEAR::isError($res)) {
1685 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1688 + $returns[$prj_id][$page] = array();
1689 + foreach ($res as $field_name => $row) {
1690 + $returns[$prj_id][$page][$field_name] = self::getColumnInfo($page, $field_name);
1691 + $returns[$prj_id][$page][$field_name]['min_role'] = $row['ctd_min_role'];
1692 + $returns[$prj_id][$page][$field_name]['rank'] = $row['ctd_rank'];
1694 + return $returns[$prj_id][$page];
1700 + * Returns the info of the column
1703 + * @param string $page The name of the page.
1704 + * @param string $column The name of the column
1705 + * @return string Info on the column
1707 + function getColumnInfo($page, $column)
1709 + $columns = self::getAllColumns($page);
1710 + return $columns[$column];
1715 + * Returns all columns available for a page
1718 + * @param string $page The name of the page
1719 + * @return array An array of columns
1721 + function getAllColumns($page)
1724 + "list_issues" => array(
1725 + "pri_rank" => array(
1726 + "title" => ev_gettext("Priority")
1728 + "iss_id" => array(
1729 + "title" => ev_gettext("Issue ID")
1731 + "usr_full_name" => array(
1732 + "title" => ev_gettext("Reporter")
1734 + "iss_created_date" => array(
1735 + "title" => ev_gettext("Created Date")
1737 + "iss_grp_id" => array(
1738 + "title" => ev_gettext("Group")
1740 + "assigned" => array(
1741 + "title" => ev_gettext("Assigned")
1743 + "time_spent" => array(
1744 + "title" => ev_gettext("Time Spent")
1746 + "iss_percent_complete" => array(
1747 + "title" => ev_gettext("% Complete"),
1748 + "default_role" => 9
1750 + "iss_dev_time" => array(
1751 + "title" => ev_gettext("Est Dev Time"),
1752 + "default_role" => 9
1754 + "prc_title" => array(
1755 + "title" => ev_gettext("Category")
1757 + "pre_title" => array(
1758 + "title" => ev_gettext("Release")
1760 + "iss_customer_id" => array(
1761 + "title" => ev_gettext("Customer")
1763 + "support_level" => array(
1764 + "title" => ev_gettext("Support Level")
1766 + "sta_rank" => array(
1767 + "title" => ev_gettext("Status")
1769 + "sta_change_date" => array(
1770 + "title" => ev_gettext("Status Change Date")
1772 + "last_action_date" => array(
1773 + "title" => ev_gettext("Last Action Date")
1775 + "custom_fields" => array(
1776 + "title" => ev_gettext("Custom Fields")
1778 + "iss_summary" => array(
1779 + "title" => ev_gettext("Summary"),
1780 + "align" => "left",
1783 + "iss_expected_resolution_date" => array(
1784 + "title" => ev_gettext("Expected Resolution Date")
1788 + return $columns[$page];
1793 + * Saves settings on which columns should be displayed.
1796 + * @return integer 1 if settings were saved successfully, -1 if there was an error.
1800 + $page = Misc::escapeString($_REQUEST['page']);
1801 + $prj_id = Misc::escapeInteger($_REQUEST['prj_id']);
1803 + $ranks = $_REQUEST['rank'];
1806 + // delete current entries
1807 + $stmt = "DELETE FROM
1808 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1810 + ctd_prj_id = $prj_id AND
1811 + ctd_page = '$page'";
1812 + $res = DB_Helper::getInstance()->query($stmt);
1813 + if (PEAR::isError($res)) {
1814 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1818 + foreach ($ranks as $field_name => $requested_rank) {
1819 + $sql = "INSERT INTO
1820 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1822 + ctd_prj_id = $prj_id,
1823 + ctd_page = '$page',
1824 + ctd_field = '$field_name',
1825 + ctd_min_role = " . $_REQUEST['min_role'][$field_name] . ",
1826 + ctd_rank = $rank";
1827 + $res = DB_Helper::getInstance()->query($sql);
1828 + if (PEAR::isError($res)) {
1829 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1839 + * Adds records in database for new project.
1841 + * @param integer $prj_id The ID of the project.
1843 + function setupNewProject($prj_id)
1845 + $page = 'list_issues';
1846 + $columns = self::getAllColumns($page);
1848 + foreach ($columns as $field_name => $column) {
1849 + if (!empty($column['default_role'])) {
1850 + $min_role = $column['default_role'];
1854 + $stmt = "INSERT INTO
1855 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1857 + ctd_prj_id = $prj_id,
1858 + ctd_page = '$page',
1859 + ctd_field = '$field_name',
1860 + ctd_min_role = $min_role,
1861 + ctd_rank = $rank";
1862 + $res = DB_Helper::getInstance()->query($stmt);
1863 + if (PEAR::isError($res)) {
1864 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1871 --- eventum-2.2/lib/eventum/class.issue.php 2009-09-14 18:07:55.000000000 +0300
1872 +++ eventum-2.2-order/lib/eventum/class.issue.php 2009-10-12 22:10:36.445851670 +0300
1873 @@ -1333,6 +1333,7 @@
1874 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1877 + self::moveOrderForAllUsers($issue_id, 1000);
1878 $prj_id = self::getProjectID($issue_id);
1880 // record the change
1881 @@ -1636,6 +1637,180 @@
1886 + * Method used to update the a single detail field of a specific issue.
1888 + * @param integer $issue_id
1889 + * @param string $field_name
1890 + * @param string $field_value
1891 + * @param string $field_type string or integer (for escape)
1892 + * @return integer 1 on success, -1 otherwise
1894 + function updateField($issue_id, $field_name, $filed_value) {
1896 + $issue_id = Misc::escapeInteger($issue_id);
1898 + $usr_id = Auth::getUserID();
1899 + $prj_id = self::getProjectID($issue_id);
1901 + // get all of the 'current' information of this issue
1902 + $current = self::getDetails($issue_id);
1905 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
1907 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
1908 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
1909 + iss_last_public_action_type='updated'";
1911 + switch ($field_name) {
1913 + $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
1916 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
1918 + case 'expected_resolution_date':
1919 + if (is_null($filed_value)) {
1920 + $stmt .= ", iss_expected_resolution_date = null";
1922 + $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
1926 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
1929 + $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
1932 + $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
1934 + case 'resolution':
1935 + $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
1938 + $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
1940 + case 'description':
1941 + $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
1943 + case 'estimated_dev_time':
1944 + $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
1946 + case 'percent_complete':
1947 + $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
1949 + case 'trigger_reminders':
1950 + $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
1953 + $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
1956 + $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
1959 + Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
1966 + iss_id=$issue_id";
1968 + $res = DB_Helper::getInstance()->query($stmt);
1969 + if (PEAR::isError($res)) {
1970 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1974 + 'category' => $current['iss_prc_id'],
1975 + 'release' => $current['iss_pre_id'],
1976 + 'expected_resolution_date' => $current['iss_expected_resolution_date'],
1977 + 'release' => $current['iss_pre_id'],
1978 + 'priority' => $current['iss_pri_id'],
1979 + 'status' => $current['iss_sta_id'],
1980 + 'resolution' => $current['iss_res_id'],
1981 + 'summary' => $current['iss_summary'],
1982 + 'description' => $current['iss_description'],
1983 + 'estimated_dev_time' => $current['iss_dev_time'],
1984 + 'percent_complete' => $current['iss_percent_complete'],
1985 + 'trigger_reminders' => $current['iss_trigger_reminders'],
1986 + 'group' => $current['iss_grp_id'],
1987 + 'iss_private' => $current['private']
1989 + $new[$field_name] = $filed_value;
1991 + // add change to the history (only for changes on specific fields?)
1992 + $updated_fields = array();
1993 + if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
1994 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
1996 + if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
1997 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
1999 + if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
2000 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
2002 + if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
2003 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
2004 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
2006 + if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
2007 + // clear out the last-triggered-reminder flag when changing the status of an issue
2008 + Reminder_Action::clearLastTriggered($issue_id);
2010 + // if old status was closed and new status is not, clear closed data from issue.
2011 + $old_status_details = Status::getDetails($current['iss_sta_id']);
2012 + if ($old_status_details['sta_is_closed'] == 1) {
2013 + $new_status_details = Status::getDetails($filed_value);
2014 + if ($new_status_details['sta_is_closed'] != 1) {
2015 + self::clearClosed($issue_id);
2018 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
2020 + if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
2021 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
2023 + if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
2024 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
2026 + if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
2027 + $updated_fields["Summary"] = '';
2029 + if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
2030 + $updated_fields["Description"] = '';
2032 + if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
2033 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
2035 + if (count($updated_fields) > 0) {
2036 + // log the changes
2039 + foreach ($updated_fields as $key => $value) {
2043 + if (($key != "Summary") && ($key != "Description")) {
2044 + $changes .= "$key: $value";
2046 + $changes .= "$key";
2051 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
2052 + // send notifications for the issue being updated
2053 + Notification::notifyIssueUpdated($issue_id, $current, $new);
2061 * Move the issue to a new project
2062 @@ -1800,16 +1975,33 @@
2064 $issue_id = Misc::escapeInteger($issue_id);
2065 $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
2067 + // move all orders down to free "order space" for this new association
2069 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2071 + isu_order = isu_order + 1
2073 + isu_usr_id = $assignee_usr_id AND
2074 + isu_order >= $order";
2075 + $res = DB_Helper::getInstance()->query($stmt);
2076 + if (PEAR::isError($res)) {
2077 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2080 + // insert the new association
2081 $stmt = "INSERT INTO
2082 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2087 + isu_assigned_date,
2092 - '" . Date_Helper::getCurrentDateGMT() . "'
2093 + '" . Date_Helper::getCurrentDateGMT() . "',
2096 $res = DB_Helper::getInstance()->query($stmt);
2097 if (PEAR::isError($res)) {
2098 @@ -1824,6 +2016,78 @@
2103 + * Method used to get the order list to be rearranged
2106 + * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
2107 + * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
2108 + * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
2110 + function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
2112 + // find all affected associantion orders
2113 + $stmt = "SELECT isu_usr_id, isu_order FROM
2114 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2116 + isu_iss_id IN ($issue_id)";
2117 + if (!empty($usr_id)) {
2118 + $stmt.= " AND isu_usr_id IN ($usr_id)";
2120 + $stmt.= "ORDER BY isu_order";
2121 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2122 + if (PEAR::isError($res)) {
2123 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2126 + $deleted_orders = array();
2127 + foreach ($res as $row) {
2128 + if (empty($deleted_orders[$row['isu_usr_id']])) {
2129 + $deleted_orders[$row['isu_usr_id']] = array();
2131 + $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
2133 + return $deleted_orders;
2139 + * Method used to rearrange order list in the db according to known deleted records
2142 + * @param mixed deleteorder list
2145 + function rearrangeDeleteUserAssociationOrderList($delete_order_list)
2147 + if (empty($delete_order_list) || (!is_array($delete_order_list))) {
2150 + foreach ($delete_order_list as $isu_usr_id => $orders) {
2151 + for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
2152 + // move the orders after them up to take the "order space" of the deleted records
2154 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2156 + isu_order = isu_order - " . ($i+1) . "
2158 + isu_usr_id = $isu_usr_id AND
2159 + isu_order > " . $orders[$i];
2160 + if ($i < count($orders) - 1) {
2162 + isu_order < " . $orders[$i+1];
2164 + $res = DB_Helper::getInstance()->query($stmt);
2165 + if (PEAR::isError($res)) {
2166 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2176 * Method used to delete all user assignments for a specific issue.
2177 @@ -1839,6 +2103,7 @@
2178 if (is_array($issue_id)) {
2179 $issue_id = implode(", ", $issue_id);
2181 + $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
2182 $stmt = "DELETE FROM
2183 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2185 @@ -1869,6 +2134,7 @@
2187 $issue_id = Misc::escapeInteger($issue_id);
2188 $usr_id = Misc::escapeInteger($usr_id);
2189 + $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
2190 $stmt = "DELETE FROM
2191 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2193 @@ -1883,6 +2149,7 @@
2194 History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
2195 User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
2197 + self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
2201 @@ -2342,6 +2609,11 @@
2203 $sort_by = self::getParam('sort_by');
2204 $sort_order = self::getParam('sort_order');
2205 + $users = self::getParam('users');
2206 + if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
2208 + unset($sort_order);
2210 $rows = self::getParam('rows');
2211 $hide_closed = self::getParam('hide_closed');
2212 if ($hide_closed === '') {
2213 @@ -2448,6 +2720,7 @@
2214 "iss_expected_resolution_date" => "desc",
2215 "pre_title" => "asc",
2216 "assigned" => "asc",
2217 + "isu_order" => "desc",
2220 foreach ($custom_fields as $fld_id => $fld_name) {
2221 @@ -3253,6 +3526,8 @@
2222 $ids = implode(", ", $ids);
2229 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
2230 @@ -3264,6 +3539,7 @@
2231 if (PEAR::isError($res)) {
2232 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2234 + // gather names of the users assigned to each issue
2236 for ($i = 0; $i < count($res); $i++) {
2237 if (!empty($t[$res[$i]['isu_iss_id']])) {
2238 @@ -3272,9 +3548,18 @@
2239 $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
2244 + for ($i = 0; $i < count($res); $i++) {
2245 + if (empty($o[$res[$i]['isu_iss_id']])) {
2246 + $o[$res[$i]['isu_iss_id']] = array();
2248 + $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
2250 // now populate the $result variable again
2251 for ($i = 0; $i < count($result); $i++) {
2252 @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
2253 + @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
2257 @@ -4247,6 +4532,7 @@
2258 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2261 + self::moveOrderForAllUsers($issue_id, 1);
2265 @@ -4346,4 +4632,120 @@
2266 History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
2267 "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
2271 + * Reorders user's issues as requested by user
2273 + * @param $usr_id User to be reordered
2274 + * @param $issue_id Issue or array of issues to be moved
2275 + * @param $neworder The new order of the issues
2278 + function reorderUserIssues($usr_id, $issue_id, $neworder)
2280 + if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
2283 + if (!is_numeric($usr_id) || !is_numeric($neworder)) {
2286 + $usr_id = Misc::escapeInteger($usr_id);
2287 + $issue_id = Misc::escapeInteger($issue_id);
2288 + $neworder = Misc::escapeInteger($neworder);
2289 + if (is_array($issue_id)) {
2290 + $issue_count = count($issue_id);
2291 + $issue_id_str = implode(", ", $issue_id);
2294 + $issue_id_str = $issue_id;
2295 + $issue_id = array($issue_id);
2297 + // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
2298 + $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
2299 + self::rearrangeDeleteUserAssociationOrderList($orderlist);
2300 + // move down the orders to free the "order space" needed
2302 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2304 + isu_order = isu_order + $issue_count
2306 + isu_usr_id = $usr_id AND
2307 + isu_order >= $neworder";
2308 + $res = DB_Helper::getInstance()->query($stmt);
2309 + if (PEAR::isError($res)) {
2310 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2313 + //update the order for the issues being moved
2315 + foreach ($issue_id as $iss_id) {
2317 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2319 + isu_order = " . ($neworder + $i) . "
2321 + isu_usr_id = $usr_id AND
2322 + isu_iss_id = $iss_id";
2323 + $res = DB_Helper::getInstance()->query($stmt);
2324 + if (PEAR::isError($res)) {
2325 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2334 + * Get users issue order list
2336 + * @param $user_id User
2337 + * @param $order_list Order of the issues
2340 + function getIssueOrderByUser($usr_id) {
2342 + if (!is_numeric($usr_id)) {
2347 + isu_iss_id, isu_order
2349 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2351 + isu_usr_id = " . $usr_id ;
2353 + $order_list = array();
2355 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2357 + if (PEAR::isError($res)) {
2358 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2361 + foreach ($res as $row) {
2362 + $order_list[$row["isu_iss_id"]] = $row["isu_order"];
2365 + return $order_list;
2368 + function moveOrderForAllUsers($issue_id, $neworder)
2370 + // Move the issue to the top priority for the ppl it's assigned to
2371 + $stmt = "SELECT isu_usr_id FROM
2372 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2374 + isu_iss_id = " . Misc::escapeInteger($issue_id);
2375 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2376 + if (PEAR::isError($res)) {
2377 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2380 + foreach ($res as $row) {
2381 + self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
2386 --- eventum-2.2/lib/eventum/class.issue.php.~1~ 1970-01-01 02:00:00.000000000 +0200
2387 +++ eventum-2.2-order/lib/eventum/class.issue.php.~1~ 2009-09-14 18:07:55.000000000 +0300
2390 +/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
2391 +// +----------------------------------------------------------------------+
2392 +// | Eventum - Issue Tracking System |
2393 +// +----------------------------------------------------------------------+
2394 +// | Copyright (c) 2003 - 2008 MySQL AB |
2395 +// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
2397 +// | This program is free software; you can redistribute it and/or modify |
2398 +// | it under the terms of the GNU General Public License as published by |
2399 +// | the Free Software Foundation; either version 2 of the License, or |
2400 +// | (at your option) any later version. |
2402 +// | This program is distributed in the hope that it will be useful, |
2403 +// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
2404 +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2405 +// | GNU General Public License for more details. |
2407 +// | You should have received a copy of the GNU General Public License |
2408 +// | along with this program; if not, write to: |
2410 +// | Free Software Foundation, Inc. |
2411 +// | 59 Temple Place - Suite 330 |
2412 +// | Boston, MA 02111-1307, USA. |
2413 +// +----------------------------------------------------------------------+
2414 +// | Authors: João Prado Maia <jpm@mysql.com> |
2415 +// +----------------------------------------------------------------------+
2420 + * Class designed to handle all business logic related to the issues in the
2421 + * system, such as adding or updating them or listing them in the grid mode.
2423 + * @author João Prado Maia <jpm@mysql.com>
2424 + * @version $Revision$
2430 + * Method used to check whether a given issue ID exists or not.
2433 + * @param integer $issue_id The issue ID
2434 + * @param boolean $check_project If we should check that this issue is in the current project
2437 + function exists($issue_id, $check_project = true)
2442 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2444 + iss_id=" . Misc::escapeInteger($issue_id);
2445 + if ($check_project) {
2447 + iss_prj_id = " . Auth::getCurrentProject();
2449 + $res = DB_Helper::getInstance()->getOne($stmt);
2450 + if (PEAR::isError($res)) {
2451 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2464 + * Method used to get the list of column heading titles for the
2465 + * CSV export functionality of the issue listing screen.
2468 + * @param integer $prj_id The project ID
2469 + * @return array The list of column heading titles
2471 + function getColumnHeadings($prj_id)
2473 + $headings = array(
2478 + // hide the group column from the output if no
2479 + // groups are available in the database
2480 + $groups = Group::getAssocList($prj_id);
2481 + if (count($groups) > 0) {
2482 + $headings[] = 'Group';
2484 + $headings[] = 'Assigned';
2485 + $headings[] = 'Time Spent';
2486 + // hide the category column from the output if no
2487 + // categories are available in the database
2488 + $categories = Category::getAssocList($prj_id);
2489 + if (count($categories) > 0) {
2490 + $headings[] = 'Category';
2492 + if (Customer::hasCustomerIntegration($prj_id)) {
2493 + $headings[] = 'Customer';
2495 + $headings[] = 'Status';
2496 + $headings[] = 'Status Change Date';
2497 + $headings[] = 'Last Action Date';
2498 + $headings[] = 'Est. Dev. TIme';
2499 + $headings[] = 'Summary';
2500 + $headings[] = 'Expected Resolution Date';
2506 + * Method used to get the full list of date fields available to issues, to
2507 + * be used when customizing the issue listing screen in the 'last status
2508 + * change date' column.
2511 + * @param boolean $display_customer_fields Whether to include any customer related fields or not
2512 + * @return array The list of available date fields
2514 + function getDateFieldsAssocList($display_customer_fields = FALSE)
2517 + 'iss_created_date' => 'Created Date',
2518 + 'iss_updated_date' => 'Last Updated Date',
2519 + 'iss_last_response_date' => 'Last Response Date',
2520 + 'iss_closed_date' => 'Closed Date'
2522 + if ($display_customer_fields) {
2523 + $fields['iss_last_customer_action_date'] = 'Customer Action Date';
2531 + * Method used to get the full list of issue IDs and their respective
2532 + * titles associated to a given project.
2535 + * @param integer $prj_id The project ID
2536 + * @return array The list of issues
2538 + function getAssocListByProject($prj_id)
2544 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2546 + iss_prj_id=" . Misc::escapeInteger($prj_id) . "
2549 + $res = DB_Helper::getInstance()->getAssoc($stmt);
2550 + if (PEAR::isError($res)) {
2551 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2560 + * Method used to get the status of a given issue.
2563 + * @param integer $issue_id The issue ID
2564 + * @return integer The status ID
2566 + function getStatusID($issue_id)
2570 + $issue_id = Misc::escapeInteger($issue_id);
2572 + if (!empty($returns[$issue_id])) {
2573 + return $returns[$issue_id];
2579 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2581 + iss_id=$issue_id";
2582 + $res = DB_Helper::getInstance()->getOne($stmt);
2583 + if (PEAR::isError($res)) {
2584 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2587 + $returns[$issue_id] = $res;
2594 + * Records the last customer action date for a given issue ID.
2597 + * @param integer $issue_id The issue ID
2598 + * @return integer 1 if the update worked, -1 otherwise
2600 + function recordLastCustomerAction($issue_id)
2603 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2605 + iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2606 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2607 + iss_last_public_action_type='customer action'
2609 + iss_id=" . Misc::escapeInteger($issue_id);
2610 + $res = DB_Helper::getInstance()->query($stmt);
2611 + if (PEAR::isError($res)) {
2612 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2621 + * Returns the customer ID associated with the given issue ID.
2624 + * @param integer $issue_id The issue ID
2625 + * @return integer The customer ID associated with the issue
2627 + function getCustomerID($issue_id)
2631 + $issue_id = Misc::escapeInteger($issue_id);
2633 + if (!empty($returns[$issue_id])) {
2634 + return $returns[$issue_id];
2640 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2642 + iss_id=$issue_id";
2643 + $res = DB_Helper::getInstance()->getOne($stmt);
2644 + if (PEAR::isError($res)) {
2645 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2648 + $returns[$issue_id] = $res;
2655 + * Returns the contract ID associated with the given issue ID.
2658 + * @param integer $issue_id The issue ID
2659 + * @return integer The customer ID associated with the issue
2661 + function getContractID($issue_id)
2665 + $issue_id = Misc::escapeInteger($issue_id);
2667 + if (!empty($returns[$issue_id])) {
2668 + return $returns[$issue_id];
2672 + iss_customer_contract_id
2674 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2676 + iss_id=$issue_id";
2677 + $res = DB_Helper::getInstance()->getOne($stmt);
2678 + if (PEAR::isError($res)) {
2679 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2682 + $returns[$issue_id] = $res;
2689 + * Sets the contract ID for a specific issue.
2692 + * @param integer $issue_id The issue ID
2693 + * @param integer The contract ID
2694 + * @return integer 1 if the update worked, -1 otherwise
2696 + function setContractID($issue_id, $contract_id)
2698 + $issue_id = Misc::escapeInteger($issue_id);
2700 + $old_contract_id = self::getContractID($issue_id);
2703 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2705 + iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
2707 + iss_id=$issue_id";
2708 + $res = DB_Helper::getInstance()->query($stmt);
2709 + if (PEAR::isError($res)) {
2710 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2714 + History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
2721 + * Returns the customer ID associated with the given issue ID.
2724 + * @param integer $issue_id The issue ID
2725 + * @return integer The customer ID associated with the issue
2727 + function getContactID($issue_id)
2731 + $issue_id = Misc::escapeInteger($issue_id);
2733 + if (!empty($returns[$issue_id])) {
2734 + return $returns[$issue_id];
2738 + iss_customer_contact_id
2740 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2742 + iss_id=$issue_id";
2743 + $res = DB_Helper::getInstance()->getOne($stmt);
2744 + if (PEAR::isError($res)) {
2745 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2748 + $returns[$issue_id] = $res;
2755 + * Method used to get the project associated to a given issue.
2758 + * @param integer $issue_id The issue ID
2759 + * @param boolean $force_refresh If the cache should not be used.
2760 + * @return integer The project ID
2762 + function getProjectID($issue_id, $force_refresh = false)
2766 + $issue_id = Misc::escapeInteger($issue_id);
2768 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
2769 + return $returns[$issue_id];
2775 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2777 + iss_id=$issue_id";
2778 + $res = DB_Helper::getInstance()->getOne($stmt);
2779 + if (PEAR::isError($res)) {
2780 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2783 + $returns[$issue_id] = $res;
2790 + * Method used to remotely assign a given issue to an user.
2793 + * @param integer $issue_id The issue ID
2794 + * @param integer $usr_id The user ID of the person performing the change
2795 + * @param boolean $assignee The user ID of the assignee
2796 + * @return integer The status ID
2798 + function remoteAssign($issue_id, $usr_id, $assignee)
2800 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
2801 + // clear up the assignments for this issue, and then assign it to the current user
2802 + self::deleteUserAssociations($issue_id, $usr_id);
2803 + $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
2805 + // save a history entry about this...
2806 + History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
2807 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
2808 + if ($assignee != $usr_id) {
2809 + Notification::notifyNewAssignment(array($assignee), $issue_id);
2817 + * Method used to set the status of a given issue.
2820 + * @param integer $issue_id The issue ID
2821 + * @param integer $status_id The new status ID
2822 + * @param boolean $notify If a notification should be sent about this change.
2823 + * @return integer 1 if the update worked, -1 otherwise
2825 + function setStatus($issue_id, $status_id, $notify = false)
2827 + $issue_id = Misc::escapeInteger($issue_id);
2828 + $status_id = Misc::escapeInteger($status_id);
2830 + $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
2831 + if ($workflow !== true) {
2835 + // check if the status is already set to the 'new' one
2836 + if (self::getStatusID($issue_id) == $status_id) {
2840 + $old_status = self::getStatusID($issue_id);
2841 + $old_details = Status::getDetails($old_status);
2844 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2846 + iss_sta_id=$status_id,
2847 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
2848 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2849 + iss_last_public_action_type='update'
2851 + iss_id=$issue_id";
2852 + $res = DB_Helper::getInstance()->query($stmt);
2853 + if (PEAR::isError($res)) {
2854 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2857 + // clear out the last-triggered-reminder flag when changing the status of an issue
2858 + Reminder_Action::clearLastTriggered($issue_id);
2860 + // if old status was closed and new status is not, clear closed data from issue.
2861 + if (@$old_details['sta_is_closed'] == 1) {
2862 + $new_details = Status::getDetails($status_id);
2863 + if ($new_details['sta_is_closed'] != 1) {
2864 + self::clearClosed($issue_id);
2869 + Notification::notifyStatusChange($issue_id, $old_status, $status_id);
2878 + * Method used to remotely set the status of a given issue.
2881 + * @param integer $issue_id The issue ID
2882 + * @param integer $usr_id The user ID of the person performing this change
2883 + * @param integer $new_status The new status ID
2884 + * @return integer 1 if the update worked, -1 otherwise
2886 + function setRemoteStatus($issue_id, $usr_id, $new_status)
2888 + $sta_id = Status::getStatusID($new_status);
2890 + $res = self::setStatus($issue_id, $sta_id);
2892 + // record history entry
2893 + History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
2900 + * Method used to set the release of an issue
2903 + * @param integer $issue_id The ID of the issue
2904 + * @param integer $pre_id The ID of the release to set this issue too
2905 + * @return integer 1 if the update worked, -1 otherwise
2907 + function setRelease($issue_id, $pre_id)
2909 + $issue_id = Misc::escapeInteger($issue_id);
2910 + $pre_id = Misc::escapeInteger($pre_id);
2912 + if ($pre_id != self::getRelease($issue_id)) {
2914 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2916 + iss_pre_id = $pre_id
2918 + iss_id = $issue_id";
2919 + $res = DB_Helper::getInstance()->query($sql);
2920 + if (PEAR::isError($res)) {
2921 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2931 + * Returns the current release of an issue
2934 + * @param integer $issue_id The ID of the issue
2935 + * @return integer The release
2937 + function getRelease($issue_id)
2942 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2944 + iss_id = " . Misc::escapeInteger($issue_id);
2945 + $res = DB_Helper::getInstance()->getOne($sql);
2946 + if (PEAR::isError($res)) {
2947 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2956 + * Method used to set the priority of an issue
2959 + * @param integer $issue_id The ID of the issue
2960 + * @param integer $pri_id The ID of the priority to set this issue too
2961 + * @return integer 1 if the update worked, -1 otherwise
2963 + function setPriority($issue_id, $pri_id)
2965 + $issue_id = Misc::escapeInteger($issue_id);
2966 + $pri_id = Misc::escapeInteger($pri_id);
2968 + if ($pri_id != self::getPriority($issue_id)) {
2970 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2972 + iss_pri_id = $pri_id
2974 + iss_id = $issue_id";
2975 + $res = DB_Helper::getInstance()->query($sql);
2976 + if (PEAR::isError($res)) {
2977 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2987 + * Returns the current issue priority
2990 + * @param integer $issue_id The ID of the issue
2991 + * @return integer The priority
2993 + function getPriority($issue_id)
2998 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3000 + iss_id = " . Misc::escapeInteger($issue_id);
3001 + $res = DB_Helper::getInstance()->getOne($sql);
3002 + if (PEAR::isError($res)) {
3003 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3012 + * Method used to set the category of an issue
3015 + * @param integer $issue_id The ID of the issue
3016 + * @param integer $prc_id The ID of the category to set this issue too
3017 + * @return integer 1 if the update worked, -1 otherwise
3019 + function setCategory($issue_id, $prc_id)
3021 + $issue_id = Misc::escapeInteger($issue_id);
3022 + $prc_id = Misc::escapeInteger($prc_id);
3024 + if ($prc_id != self::getPriority($issue_id)) {
3026 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3028 + iss_prc_id = $prc_id
3030 + iss_id = $issue_id";
3031 + $res = DB_Helper::getInstance()->query($sql);
3032 + if (PEAR::isError($res)) {
3033 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3043 + * Returns the current issue category
3046 + * @param integer $issue_id The ID of the issue
3047 + * @return integer The category
3049 + function getCategory($issue_id)
3054 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3056 + iss_id = " . Misc::escapeInteger($issue_id);
3057 + $res = DB_Helper::getInstance()->getOne($sql);
3058 + if (PEAR::isError($res)) {
3059 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3068 + * Method used to get all issues associated with a status that doesn't have
3069 + * the 'closed' context.
3072 + * @param integer $prj_id The project ID to list issues from
3073 + * @param integer $usr_id The user ID of the user requesting this information
3074 + * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
3075 + * @param integer $status_id The status ID to be used to restrict results
3076 + * @return array The list of open issues
3078 + function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
3080 + $prj_id = Misc::escapeInteger($prj_id);
3081 + $status_id = Misc::escapeInteger($status_id);
3082 + $projects = Project::getRemoteAssocListByUser($usr_id);
3083 + if (@count($projects) == 0) {
3093 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3094 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
3097 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
3101 + if (!empty($status_id)) {
3102 + $stmt .= " sta_id=$status_id AND ";
3105 + iss_prj_id=$prj_id AND
3106 + sta_id=iss_sta_id AND
3108 + if ($show_all_issues == false) {
3110 + isu_usr_id=$usr_id";
3112 + $stmt .= "\nGROUP BY
3114 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
3115 + if (PEAR::isError($res)) {
3116 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3119 + if (count($res) > 0) {
3120 + self::getAssignedUsersByIssues($res);
3128 + * Method used to build the required parameters to simulate an email reply
3129 + * to the user who reported the issue, using the issue details like summary
3130 + * and description as email fields.
3133 + * @param integer $issue_id The issue ID
3134 + * @return array The email parameters
3136 + function getReplyDetails($issue_id)
3138 + $issue_id = Misc::escapeInteger($issue_id);
3142 + usr_full_name AS reporter,
3143 + usr_email AS reporter_email,
3144 + iss_description AS description,
3145 + iss_summary AS sup_subject
3147 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3148 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
3150 + iss_usr_id=usr_id AND
3151 + iss_id=$issue_id";
3152 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
3153 + if (PEAR::isError($res)) {
3154 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3157 + $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
3158 + $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
3165 + * Method used to record the last updated timestamp for a given
3169 + * @param integer $issue_id The issue ID
3170 + * @param string $type The type of update that was made (optional)
3173 + function markAsUpdated($issue_id, $type = false)
3175 + $public = array("staff response", "customer action", "file uploaded", "user response");
3177 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3179 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
3180 + if ($type != false) {
3181 + if (in_array($type, $public)) {
3182 + $field = "iss_last_public_action_";
3184 + $field = "iss_last_internal_action_";
3186 + $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
3187 + $field . "type ='" . Misc::escapeString($type) . "'\n";
3190 + iss_id=" . Misc::escapeInteger($issue_id);
3191 + $res = DB_Helper::getInstance()->query($stmt);
3192 + if (PEAR::isError($res)) {
3193 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3196 + // update last response dates if this is a staff response
3197 + if ($type == "staff response") {
3199 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3201 + iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
3203 + iss_id = " . Misc::escapeInteger($issue_id);
3204 + DB_Helper::getInstance()->query($stmt);
3206 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3208 + iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
3210 + iss_first_response_date IS NULL AND
3211 + iss_id = " . Misc::escapeInteger($issue_id);
3212 + DB_Helper::getInstance()->query($stmt);
3221 + * Method used to check whether a given issue has duplicates
3225 + * @param integer $issue_id The issue ID
3228 + function hasDuplicates($issue_id)
3233 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3235 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
3236 + $res = DB_Helper::getInstance()->getOne($stmt);
3237 + if (PEAR::isError($res)) {
3238 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3251 + * Method used to update the duplicated issues for a given
3255 + * @param integer $issue_id The issue ID
3256 + * @return integer 1 if the update worked, -1 otherwise
3258 + function updateDuplicates($issue_id)
3260 + $issue_id = Misc::escapeInteger($issue_id);
3262 + $ids = self::getDuplicateList($issue_id);
3266 + $ids = @array_keys($ids);
3268 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3270 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3271 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3272 + iss_last_internal_action_type='updated',
3273 + iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
3274 + if (@$_POST["keep"] == "no") {
3275 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
3278 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
3279 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
3280 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
3282 + iss_id IN (" . implode(", ", $ids) . ")";
3283 + $res = DB_Helper::getInstance()->query($stmt);
3284 + if (PEAR::isError($res)) {
3285 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3288 + // record the change
3289 + for ($i = 0; $i < count($ids); $i++) {
3290 + History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
3291 + "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
3299 + * Method used to get a list of the duplicate issues for a given
3303 + * @param integer $issue_id The issue ID
3304 + * @return array The list of duplicates
3306 + function getDuplicateList($issue_id)
3308 + $res = self::getDuplicateDetailsList($issue_id);
3309 + if (@count($res) == 0) {
3313 + for ($i = 0; $i < count($res); $i++) {
3314 + $list[$res[$i]['issue_id']] = $res[$i]['title'];
3322 + * Method used to get a list of the duplicate issues (and their details)
3323 + * for a given issue ID.
3326 + * @param integer $issue_id The issue ID
3327 + * @return array The list of duplicates
3329 + function getDuplicateDetailsList($issue_id)
3333 + if (!empty($returns[$issue_id])) {
3334 + return $returns[$issue_id];
3339 + iss_summary title,
3340 + sta_title current_status,
3341 + sta_is_closed is_closed
3343 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3344 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
3346 + iss_sta_id=sta_id AND
3347 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
3348 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
3349 + if (PEAR::isError($res)) {
3350 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3353 + $returns[$issue_id] = $res;
3360 + * Method used to clear the duplicate status of an issue.
3363 + * @param integer $issue_id The issue ID
3364 + * @return integer 1 if the update worked, -1 otherwise
3366 + function clearDuplicateStatus($issue_id)
3368 + $issue_id = Misc::escapeInteger($issue_id);
3370 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3372 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3373 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3374 + iss_last_internal_action_type='updated',
3375 + iss_duplicated_iss_id=NULL
3377 + iss_id=$issue_id";
3378 + $res = DB_Helper::getInstance()->query($stmt);
3379 + if (PEAR::isError($res)) {
3380 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3383 + // record the change
3384 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
3391 + * Method used to mark an issue as a duplicate of an existing one.
3394 + * @param integer $issue_id The issue ID
3395 + * @return integer 1 if the update worked, -1 otherwise
3397 + function markAsDuplicate($issue_id)
3399 + $issue_id = Misc::escapeInteger($issue_id);
3400 + if (!self::exists($issue_id)) {
3405 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3407 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3408 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3409 + iss_last_internal_action_type='updated',
3410 + iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
3412 + iss_id=$issue_id";
3413 + $res = DB_Helper::getInstance()->query($stmt);
3414 + if (PEAR::isError($res)) {
3415 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3418 + if (!empty($_POST["comments"])) {
3419 + // add note with the comments of marking an issue as a duplicate of another one
3420 + $_POST['title'] = 'Issue duplication comments';
3421 + $_POST["note"] = $_POST["comments"];
3422 + Note::insert(Auth::getUserID(), $issue_id);
3424 + // record the change
3425 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
3426 + "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
3432 + function isDuplicate($issue_id)
3437 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3439 + iss_id = " . Misc::escapeInteger($issue_id) . " AND
3440 + iss_duplicated_iss_id IS NULL";
3441 + $res = DB_Helper::getInstance()->getOne($sql);
3442 + if (PEAR::isError($res)) {
3443 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3455 + * Method used to get an associative array of user ID => user
3456 + * status associated with a given issue ID.
3459 + * @param integer $issue_id The issue ID
3460 + * @return array The list of users
3462 + function getAssignedUsersStatus($issue_id)
3468 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
3469 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
3471 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
3472 + isu_usr_id=usr_id";
3473 + $res = DB_Helper::getInstance()->getAssoc($stmt);
3474 + if (PEAR::isError($res)) {
3475 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3484 + * Method used to get the summary associated with a given issue ID.
3487 + * @param integer $issue_id The issue ID
3488 + * @return string The issue summary
3490 + function getTitle($issue_id)
3495 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3497 + iss_id=" . Misc::escapeInteger($issue_id);
3498 + $res = DB_Helper::getInstance()->getOne($stmt);
3499 + if (PEAR::isError($res)) {
3500 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3509 + * Method used to get the issue ID associated with a specific summary.
3512 + * @param string $summary The summary to look for
3513 + * @return integer The issue ID
3515 + function getIssueID($summary)
3520 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3522 + iss_summary='" . Misc::escapeString($summary) . "'";
3523 + $res = DB_Helper::getInstance()->getOne($stmt);
3524 + if (PEAR::isError($res)) {
3525 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3528 + if (empty($res)) {
3538 + * Method used to add a new anonymous based issue in the system.
3541 + * @return integer The new issue ID
3543 + function addAnonymousReport()
3545 + $options = Project::getAnonymousPostOptions($_POST["project"]);
3546 + $initial_status = Project::getInitialStatus($_POST["project"]);
3547 + $stmt = "INSERT INTO
3548 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3555 + if (!empty($initial_status)) {
3556 + $stmt .= "iss_sta_id,";
3560 + iss_last_public_action_date,
3561 + iss_last_public_action_type,
3564 + iss_root_message_id
3566 + " . Misc::escapeInteger($_POST["project"]) . ",
3567 + " . $options["category"] . ",
3569 + " . $options["priority"] . ",
3570 + " . $options["reporter"] . ",";
3571 + if (!empty($initial_status)) {
3572 + $stmt .= "$initial_status,";
3575 + '" . Date_Helper::getCurrentDateGMT() . "',
3576 + '" . Date_Helper::getCurrentDateGMT() . "',
3578 + '" . Misc::escapeString($_POST["summary"]) . "',
3579 + '" . Misc::escapeString($_POST["description"]) . "',
3580 + '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
3582 + $res = DB_Helper::getInstance()->query($stmt);
3583 + if (PEAR::isError($res)) {
3584 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3587 + $new_issue_id = DB_Helper::get_last_insert_id();
3588 + // log the creation of the issue
3589 + History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
3591 + // now process any files being uploaded
3593 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
3594 + if (!@empty($_FILES["file"]["name"][$i])) {
3600 + $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
3601 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
3602 + $filename = @$_FILES["file"]["name"][$i];
3603 + if (empty($filename)) {
3606 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
3607 + if (!empty($blob)) {
3608 + Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
3612 + // need to process any custom fields ?
3613 + if (@count($_POST["custom_fields"]) > 0) {
3614 + foreach ($_POST["custom_fields"] as $fld_id => $value) {
3615 + Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
3619 + // now add the user/issue association
3620 + $assign = array();
3621 + $users = @$options["users"];
3622 + $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
3623 + for ($i = 0; $i < count($users); $i++) {
3624 + Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
3625 + self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
3626 + $assign[] = $users[$i];
3629 + Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
3631 + // also notify any users that want to receive emails anytime a new issue is created
3632 + Notification::notifyNewIssue($_POST['project'], $new_issue_id);
3634 + return $new_issue_id;
3640 + * Method used to remove all issues associated with a specific list of
3644 + * @param array $ids The list of projects to look for
3647 + function removeByProjects($ids)
3649 + $items = @implode(", ", Misc::escapeInteger($ids));
3653 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3655 + iss_prj_id IN ($items)";
3656 + $res = DB_Helper::getInstance()->getCol($stmt);
3657 + if (PEAR::isError($res)) {
3658 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3661 + if (count($res) > 0) {
3662 + self::deleteAssociations($res);
3663 + Attachment::removeByIssues($res);
3664 + SCM::removeByIssues($res);
3665 + Impact_Analysis::removeByIssues($res);
3666 + self::deleteUserAssociations($res);
3667 + Note::removeByIssues($res);
3668 + Time_Tracking::removeByIssues($res);
3669 + Notification::removeByIssues($res);
3670 + Custom_Field::removeByIssues($res);
3671 + Phone_Support::removeByIssues($res);
3672 + History::removeByIssues($res);
3673 + // now really delete the issues
3674 + $items = implode(", ", $res);
3675 + $stmt = "DELETE FROM
3676 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3678 + iss_id IN ($items)";
3679 + DB_Helper::getInstance()->query($stmt);
3687 + * Method used to close off an issue.
3690 + * @param integer $usr_id The user ID
3691 + * @param integer $issue_id The issue ID
3692 + * @param bool $send_notification Whether to send a notification about this action or not
3693 + * @param integer $resolution_id The resolution ID
3694 + * @param integer $status_id The status ID
3695 + * @param string $reason The reason for closing this issue
3696 + * @param string $send_notification_to Who this notification should be sent too
3697 + * @return integer 1 if the update worked, -1 otherwise
3699 + function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
3701 + $usr_id = Misc::escapeInteger($usr_id);
3702 + $issue_id = Misc::escapeInteger($issue_id);
3703 + $resolution_id = Misc::escapeInteger($resolution_id);
3704 + $status_id = Misc::escapeInteger($status_id);
3707 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3709 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3710 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3711 + iss_last_public_action_type='closed',
3712 + iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
3713 + if (!empty($resolution_id)) {
3714 + $stmt .= "iss_res_id=$resolution_id,\n";
3716 + $stmt .= "iss_sta_id=$status_id
3718 + iss_id=$issue_id";
3719 + $res = DB_Helper::getInstance()->query($stmt);
3720 + if (PEAR::isError($res)) {
3721 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3724 + $prj_id = self::getProjectID($issue_id);
3726 + // record the change
3727 + History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
3729 + if ($send_notification_to == 'all') {
3731 + $from = User::getFromHeader($usr_id);
3732 + $message_id = User::getFromHeader($usr_id);
3733 + $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
3734 + '', '', 'Issue closed comments', $reason, '');
3736 + $structure = Mime_Helper::decode($full_email, true, false);
3739 + 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
3740 + 'issue_id' => $issue_id,
3741 + 'message_id' => $message_id,
3742 + 'date' => Date_Helper::getCurrentDateGMT(),
3743 + 'subject' => 'Issue closed comments',
3745 + 'has_attachment'=> 0,
3746 + 'body' => $reason,
3747 + 'full_email' => $full_email,
3748 + 'headers' => $structure->headers
3750 + Support::insertEmail($email, $structure, $sup_id, true);
3753 + // add note with the reason to close the issue
3754 + $_POST['title'] = 'Issue closed comments';
3755 + $_POST["note"] = $reason;
3756 + Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
3760 + if ($send_notification) {
3761 + if (Customer::hasCustomerIntegration($prj_id)) {
3762 + // send a special confirmation email when customer issues are closed
3764 + iss_customer_contact_id
3766 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3768 + iss_id=$issue_id";
3769 + $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
3770 + if (!empty($customer_contact_id)) {
3771 + Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
3774 + // send notifications for the issue being closed
3775 + Notification::notify($issue_id, 'closed', $ids);
3777 + Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
3784 + * Method used to update the details of a specific issue.
3787 + * @param integer $issue_id The issue ID
3788 + * @return integer 1 if the update worked, -1 or -2 otherwise
3790 + function update($issue_id)
3793 + $errors = array();
3795 + $issue_id = Misc::escapeInteger($issue_id);
3797 + $usr_id = Auth::getUserID();
3798 + $prj_id = self::getProjectID($issue_id);
3800 + $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
3801 + if ($workflow !== true) {
3805 + // get all of the 'current' information of this issue
3806 + $current = self::getDetails($issue_id);
3807 + // update the issue associations
3808 + if (empty($_POST['associated_issues'])) {
3809 + $associated_issues = array();
3811 + $associated_issues = explode(',', @$_POST['associated_issues']);
3812 + // make sure all associated issues are valid (and in this project)
3813 + for ($i = 0; $i < count($associated_issues); $i++) {
3814 + if (!self::exists(trim($associated_issues[$i]), false)) {
3815 + $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
3816 + unset($associated_issues[$i]);
3820 + $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
3821 + if (count($association_diff) > 0) {
3822 + // go through the new assocations, if association already exists, skip it
3823 + $associations_to_remove = $current['associated_issues'];
3824 + if (count($associated_issues) > 0) {
3825 + foreach ($associated_issues as $index => $associated_id) {
3826 + if (!in_array($associated_id, $current['associated_issues'])) {
3827 + self::addAssociation($issue_id, $associated_id, $usr_id);
3829 + // already assigned, remove this user from list of users to remove
3830 + unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
3834 + if (count($associations_to_remove) > 0) {
3835 + foreach ($associations_to_remove as $associated_id) {
3836 + self::deleteAssociation($issue_id, $associated_id);
3840 + $assignments_changed = false;
3841 + if (@$_POST["keep_assignments"] == "no") {
3842 + // only change the issue-user associations if there really were any changes
3843 + $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
3844 + if (!empty($_POST['assignments'])) {
3845 + $new_assignees = @$_POST['assignments'];
3847 + $new_assignees = array();
3849 + $assignment_notifications = array();
3851 + // remove people from the assignment list, if appropriate
3852 + foreach ($old_assignees as $assignee) {
3853 + if (!in_array($assignee, $new_assignees)) {
3854 + self::deleteUserAssociation($issue_id, $assignee);
3855 + $assignments_changed = true;
3858 + // add people to the assignment list, if appropriate
3859 + foreach ($new_assignees as $assignee) {
3860 + if (!in_array($assignee, $old_assignees)) {
3861 + self::addUserAssociation($usr_id, $issue_id, $assignee);
3862 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
3863 + $assignment_notifications[] = $assignee;
3864 + $assignments_changed = true;
3867 + if (count($assignment_notifications) > 0) {
3868 + Notification::notifyNewAssignment($assignment_notifications, $issue_id);
3871 + if (empty($_POST["estimated_dev_time"])) {
3872 + $_POST["estimated_dev_time"] = 0;
3875 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3877 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3878 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3879 + iss_last_public_action_type='updated',";
3880 + if (!empty($_POST["category"])) {
3881 + $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
3883 + if (@$_POST["keep"] == "no") {
3884 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
3886 + if (!empty($_POST['expected_resolution_date'])) {
3887 + $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
3889 + $stmt .= "iss_expected_resolution_date=null,";
3892 + iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
3893 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
3894 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
3895 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
3896 + iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
3897 + iss_description='" . Misc::escapeString($_POST["description"]) . "',
3898 + iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
3899 + iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
3900 + iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
3901 + iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
3902 + if (isset($_POST['private'])) {
3904 + iss_private = " . Misc::escapeInteger($_POST['private']);
3908 + iss_id=$issue_id";
3909 + $res = DB_Helper::getInstance()->query($stmt);
3910 + if (PEAR::isError($res)) {
3911 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3914 + // add change to the history (only for changes on specific fields?)
3915 + $updated_fields = array();
3916 + if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
3917 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
3919 + if ($current["iss_prc_id"] != $_POST["category"]) {
3920 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
3922 + if ($current["iss_pre_id"] != $_POST["release"]) {
3923 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
3925 + if ($current["iss_pri_id"] != $_POST["priority"]) {
3926 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
3927 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
3929 + if ($current["iss_sta_id"] != $_POST["status"]) {
3930 + // clear out the last-triggered-reminder flag when changing the status of an issue
3931 + Reminder_Action::clearLastTriggered($issue_id);
3933 + // if old status was closed and new status is not, clear closed data from issue.
3934 + $old_status_details = Status::getDetails($current['iss_sta_id']);
3935 + if ($old_status_details['sta_is_closed'] == 1) {
3936 + $new_status_details = Status::getDetails($_POST["status"]);
3937 + if ($new_status_details['sta_is_closed'] != 1) {
3938 + self::clearClosed($issue_id);
3941 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
3943 + if ($current["iss_res_id"] != $_POST["resolution"]) {
3944 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
3946 + if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
3947 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
3949 + if ($current["iss_summary"] != $_POST["summary"]) {
3950 + $updated_fields["Summary"] = '';
3952 + if ($current["iss_description"] != $_POST["description"]) {
3953 + $updated_fields["Description"] = '';
3955 + if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
3956 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
3958 + if (count($updated_fields) > 0) {
3959 + // log the changes
3962 + foreach ($updated_fields as $key => $value) {
3966 + if (($key != "Summary") && ($key != "Description")) {
3967 + $changes .= "$key: $value";
3969 + $changes .= "$key";
3973 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
3974 + // send notifications for the issue being updated
3975 + Notification::notifyIssueUpdated($issue_id, $current, $_POST);
3978 + // record group change as a seperate change
3979 + if ($current["iss_grp_id"] != (int)$_POST["group"]) {
3980 + History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
3981 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
3984 + // now update any duplicates, if any
3985 + $update_dupe = array(
3992 + // COMPAT: the following line requires PHP > 4.0.4
3993 + $intersect = array_intersect($update_dupe, array_keys($updated_fields));
3994 + if (($current["duplicates"] != '') && (count($intersect) > 0)) {
3995 + self::updateDuplicates($issue_id);
3998 + // if there is customer integration, mark last customer action
3999 + if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
4000 + self::recordLastCustomerAction($issue_id);
4003 + if ($assignments_changed) {
4004 + // XXX: we may want to also send the email notification for those "new" assignees
4005 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
4008 + Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
4009 + // Move issue to another project
4010 + if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
4011 + $new_prj_id = (int)@$_POST['new_prj'];
4012 + if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
4013 + if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
4014 + $res = self::moveIssue($issue_id, $new_prj_id);
4029 + * Move the issue to a new project
4031 + * @param integer $issue_id
4032 + * @param integer $new_prj_id
4033 + * @return integer 1 on success, -1 otherwise
4035 + function moveIssue($issue_id, $new_prj_id)
4038 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
4040 + iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
4042 + iss_id = " . Misc::escapeInteger($issue_id);
4043 + $res = DB_Helper::getInstance()->query($stmt);
4044 + if (PEAR::isError($res)) {
4045 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4048 + $currentDetails = self::getDetails($issue_id);
4050 + // set new category
4051 + $new_iss_prc_list = Category::getAssocList($new_prj_id);
4052 + $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
4053 + $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
4054 + if ($new_prc_id === false) {
4055 + // use the first category listed in the new project
4056 + $new_prc_id = key($new_iss_prc_list);
4059 + // set new priority
4060 + $new_iss_pri_list = Priority::getAssocList($new_prj_id);
4061 + $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
4062 + $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
4063 + if ($new_pri_id === false) {
4064 + // use the first category listed in the new project
4065 + $new_pri_id = key($new_iss_pri_list);
4068 + // XXX: Set status if needed when moving issue
4071 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
4073 + iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
4074 + iss_pri_id=" . $new_pri_id . "
4076 + iss_id=$issue_id";
4077 + $res = DB_Helper::getInstance()->query($stmt);
4078 + if (PEAR::isError($res)) {
4079 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4082 + // clear project cache
4083 + self::getProjectID($issue_id, true);
4085 + Notification::notifyNewIssue($new_prj_id, $issue_id);
4091 + * Method used to associate an existing issue with another one.
4094 + * @param integer $issue_id The issue ID
4095 + * @param integer $issue_id The other issue ID
4098 + function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
4100 + $issue_id = Misc::escapeInteger($issue_id);
4101 + $associated_id = Misc::escapeInteger($associated_id);
4103 + $stmt = "INSERT INTO
4104 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4112 + DB_Helper::getInstance()->query($stmt);
4113 + History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
4114 + // link the associated issue back to this one
4115 + if ($link_issues) {
4116 + self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
4122 + * Method used to remove the issue associations related to a specific issue.
4125 + * @param integer $issue_id The issue ID
4128 + function deleteAssociations($issue_id, $usr_id = FALSE)
4130 + $issue_id = Misc::escapeInteger($issue_id);
4131 + if (is_array($issue_id)) {
4132 + $issue_id = implode(", ", $issue_id);
4134 + $stmt = "DELETE FROM
4135 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4137 + isa_issue_id IN ($issue_id) OR
4138 + isa_associated_id IN ($issue_id)";
4139 + DB_Helper::getInstance()->query($stmt);
4141 + History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
4147 + * Method used to remove a issue association from an issue.
4150 + * @param integer $issue_id The issue ID
4151 + * @param integer $associated_id The associated issue ID to remove.
4154 + function deleteAssociation($issue_id, $associated_id)
4156 + $issue_id = Misc::escapeInteger($issue_id);
4157 + $associated_id = Misc::escapeInteger($associated_id);
4158 + $stmt = "DELETE FROM
4159 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4162 + isa_issue_id = $issue_id AND
4163 + isa_associated_id = $associated_id
4166 + isa_issue_id = $associated_id AND
4167 + isa_associated_id = $issue_id
4169 + DB_Helper::getInstance()->query($stmt);
4170 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
4171 + "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
4172 + History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
4173 + "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
4178 + * Method used to assign an issue with an user.
4181 + * @param integer $usr_id The user ID of the person performing this change
4182 + * @param integer $issue_id The issue ID
4183 + * @param integer $assignee_usr_id The user ID of the assignee
4184 + * @param boolean $add_history Whether to add a history entry about this or not
4185 + * @return integer 1 if the update worked, -1 otherwise
4187 + function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
4189 + $issue_id = Misc::escapeInteger($issue_id);
4190 + $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
4191 + $stmt = "INSERT INTO
4192 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4200 + '" . Date_Helper::getCurrentDateGMT() . "'
4202 + $res = DB_Helper::getInstance()->query($stmt);
4203 + if (PEAR::isError($res)) {
4204 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4207 + if ($add_history) {
4208 + History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
4209 + 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
4217 + * Method used to delete all user assignments for a specific issue.
4220 + * @param integer $issue_id The issue ID
4221 + * @param integer $usr_id The user ID of the person performing the change
4224 + function deleteUserAssociations($issue_id, $usr_id = FALSE)
4226 + $issue_id = Misc::escapeInteger($issue_id);
4227 + if (is_array($issue_id)) {
4228 + $issue_id = implode(", ", $issue_id);
4230 + $stmt = "DELETE FROM
4231 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4233 + isu_iss_id IN ($issue_id)";
4234 + $res = DB_Helper::getInstance()->query($stmt);
4235 + if (PEAR::isError($res)) {
4236 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4240 + History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
4248 + * Method used to delete a single user assignments for a specific issue.
4251 + * @param integer $issue_id The issue ID
4252 + * @param integer $usr_id The user to remove.
4253 + * @param boolean $add_history Whether to add a history entry about this or not
4256 + function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
4258 + $issue_id = Misc::escapeInteger($issue_id);
4259 + $usr_id = Misc::escapeInteger($usr_id);
4260 + $stmt = "DELETE FROM
4261 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4263 + isu_iss_id = $issue_id AND
4264 + isu_usr_id = $usr_id";
4265 + $res = DB_Helper::getInstance()->query($stmt);
4266 + if (PEAR::isError($res)) {
4267 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4270 + if ($add_history) {
4271 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
4272 + User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
4280 + * Creates an issue with the given email information.
4283 + * @param integer $prj_id The project ID
4284 + * @param integer $usr_id The user responsible for this action
4285 + * @param string $sender The original sender of this email
4286 + * @param string $summary The issue summary
4287 + * @param string $description The issue description
4288 + * @param integer $category The category ID
4289 + * @param integer $priority The priority ID
4290 + * @param array $assignment The list of users to assign this issue to
4291 + * @param string $date The date the email was originally sent.
4292 + * @param string $msg_id The message ID of the email we are creating this issue from.
4295 + function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
4298 + $exclude_list = array();
4300 + $sender_email = Mail_Helper::getEmailAddress($sender);
4301 + $sender_usr_id = User::getUserIDByEmail($sender_email, true);
4302 + if (!empty($sender_usr_id)) {
4303 + $reporter = $sender_usr_id;
4304 + $exclude_list[] = $sender_usr_id;
4308 + 'category' => $category,
4309 + 'priority' => $priority,
4310 + 'description' => $description,
4311 + 'summary' => $summary,
4312 + 'msg_id' => $msg_id,
4315 + if (Customer::hasCustomerIntegration($prj_id)) {
4316 + list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
4317 + if (!empty($customer_id)) {
4318 + $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
4319 + // overwrite the reporter with the customer contact
4320 + $reporter = User::getUserIDByContactID($customer_contact_id);
4321 + $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
4323 + $data['customer'] = $customer_id;
4324 + $data['contact'] = $customer_contact_id;
4325 +# $data['contract'] = // XXX missing
4326 + $data['contact_person_lname'] = $contact['last_name'];
4327 + $data['contact_person_fname'] = $contact['first_name'];
4328 + $data['contact_email'] = $sender_email;
4329 + $data['contact_phone'] = $contact['phone'];
4330 + $data['contact_timezone'] = $contact_timezone;
4333 + $customer_id = FALSE;
4335 + if (empty($reporter)) {
4336 + $reporter = APP_SYSTEM_USER_ID;
4339 + $data['reporter'] = $reporter;
4341 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
4342 + if ($issue_id == -1) {
4348 + // log the creation of the issue
4349 + History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
4351 + $emails = array();
4352 + $manager_usr_ids = array();
4353 + if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
4354 + // if there are any technical account managers associated with this customer, add these users to the notification list
4355 + $managers = Customer::getAccountManagers($prj_id, $customer_id);
4356 + $manager_usr_ids = array_keys($managers);
4357 + $manager_emails = array_values($managers);
4358 + $emails = array_merge($emails, $manager_emails);
4360 + // add the reporter to the notification list
4361 + $emails[] = $sender;
4362 + $emails = array_unique($emails);
4363 + $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
4364 + foreach ($emails as $address) {
4365 + Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
4368 + // only assign the issue to an user if the associated customer has any technical account managers
4370 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
4371 + foreach ($manager_usr_ids as $manager_usr_id) {
4372 + $users[] = $manager_usr_id;
4373 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
4374 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
4378 + // now add the user/issue association
4379 + if (@count($assignment) > 0) {
4380 + for ($i = 0; $i < count($assignment); $i++) {
4381 + Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
4382 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
4383 + if ($assignment[$i] != $usr_id) {
4384 + $users[] = $assignment[$i];
4388 + // only use the round-robin feature if this new issue was not
4389 + // already assigned to a customer account manager
4390 + if (@count($manager_usr_ids) < 1) {
4391 + $assignee = Round_Robin::getNextAssignee($prj_id);
4392 + // assign the issue to the round robin person
4393 + if (!empty($assignee)) {
4394 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
4395 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
4396 + $users[] = $assignee;
4401 + if (count($users) > 0) {
4402 + $has_assignee = true;
4405 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
4407 + // send special 'an issue was auto-created for you' notification back to the sender
4408 + Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
4410 + // also notify any users that want to receive emails anytime a new issue is created
4411 + Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
4418 + * Return errors that happened when creating new issue from POST method.
4422 + private static $insert_errors = array();
4423 + static function getInsertErrors() {
4424 + return self::$insert_errors;
4428 + * Method used to add a new issue using the normal report form.
4431 + * @return integer The new issue ID
4433 + function createFromPost()
4436 + 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
4437 + 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
4438 + 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
4441 + foreach ($keys as $key) {
4442 + if (isset($_POST[$key])) {
4443 + $data[$key] = $_POST[$key];
4447 + $prj_id = Auth::getCurrentProject();
4448 + $usr_id = Auth::getUserID();
4450 + // if we are creating an issue for a customer, put the
4451 + // main customer contact as the reporter for it
4452 + if (Customer::hasCustomerIntegration($prj_id)) {
4453 + $contact_usr_id = User::getUserIDByContactID($data['contact']);
4454 + if (empty($contact_usr_id)) {
4455 + $contact_usr_id = $usr_id;
4457 + $data['reporter'] = $contact_usr_id;
4459 + $data['reporter'] = $usr_id;
4462 + $data['msg_id'] = Mail_Helper::generateMessageID();
4464 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
4465 + if ($issue_id == -1) {
4471 + $info = User::getNameEmail($usr_id);
4472 + // log the creation of the issue
4473 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
4475 + $emails = array();
4476 + if (Customer::hasCustomerIntegration($prj_id)) {
4477 + if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
4478 + $emails = $data['contact_extra_emails'];
4480 + // add the primary contact to the notification list
4481 + if ($data['add_primary_contact'] == 'yes') {
4482 + $contact_email = User::getEmailByContactID($data['contact']);
4483 + if (!empty($contact_email)) {
4484 + $emails[] = $contact_email;
4487 + // if there are any technical account managers associated with this customer, add these users to the notification list
4488 + $managers = Customer::getAccountManagers($prj_id, $data['customer']);
4489 + $manager_usr_ids = array_keys($managers);
4490 + $manager_emails = array_values($managers);
4491 + $emails = array_merge($emails, $manager_emails);
4493 + // add the reporter to the notification list
4494 + $emails[] = $info['usr_email'];
4495 + $emails = array_unique($emails);
4496 + foreach ($emails as $address) {
4497 + Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
4500 + // only assign the issue to an user if the associated customer has any technical account managers
4503 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
4504 + foreach ($manager_usr_ids as $manager_usr_id) {
4505 + $users[] = $manager_usr_id;
4506 + self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
4507 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
4511 + // now add the user/issue association (aka assignments)
4512 + if (!empty($data['users']) && count($data['users']) > 0) {
4513 + for ($i = 0; $i < count($data['users']); $i++) {
4514 + Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
4515 + Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
4516 + self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
4517 + if ($data['users'][$i] != $usr_id) {
4518 + $users[] = $data['users'][$i];
4522 + // only use the round-robin feature if this new issue was not
4523 + // already assigned to a customer account manager
4524 + if (@count($manager_usr_ids) < 1) {
4525 + $assignee = Round_Robin::getNextAssignee($prj_id);
4526 + // assign the issue to the round robin person
4527 + if (!empty($assignee)) {
4528 + $users[] = $assignee;
4529 + self::addUserAssociation($usr_id, $issue_id, $assignee, false);
4530 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
4536 + // now process any files being uploaded
4538 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
4539 + if (!@empty($_FILES["file"]["name"][$i])) {
4546 + for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
4547 + $filename = @$_FILES["file"]["name"][$i];
4548 + if (empty($filename)) {
4551 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
4552 + if (empty($blob)) {
4553 + // error reading a file
4554 + self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
4558 + "filename" => $filename,
4559 + "type" => $_FILES['file']['type'][$i],
4563 + if (count($files) > 0) {
4564 + $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
4565 + foreach ($files as $file) {
4566 + Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
4570 + // need to associate any emails ?
4571 + if (!empty($data['attached_emails'])) {
4572 + $items = explode(",", $data['attached_emails']);
4573 + Support::associate($usr_id, $issue_id, $items);
4575 + // need to notify any emails being converted into issues ?
4576 + if (@count($data['notify_senders']) > 0) {
4577 + $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
4579 + $recipients = array();
4581 + // need to process any custom fields ?
4582 + if (@count($data['custom_fields']) > 0) {
4583 + foreach ($data['custom_fields'] as $fld_id => $value) {
4584 + Custom_Field::associateIssue($issue_id, $fld_id, $value);
4587 + // also send a special confirmation email to the customer contact
4588 + if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
4589 + // also need to pass the list of sender emails already notified,
4590 + // so we can avoid notifying the same person again
4591 + $contact_email = User::getEmailByContactID($data['contact']);
4592 + if (@!in_array($contact_email, $recipients)) {
4593 + Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
4597 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
4599 + // also notify any users that want to receive emails anytime a new issue is created
4600 + Notification::notifyNewIssue($prj_id, $issue_id);
4606 + * Insert issue to database.
4608 + * @param integer $prj_id The project ID
4609 + * @param integer $usr_id The user responsible for this action
4610 + * @param array $data of issue to be inserted
4611 + * @return integer The new issue ID
4613 + private function insertIssue($prj_id, $usr_id, $data)
4616 + // XXX missing_fields never used
4617 + $missing_fields = array();
4618 + if ($data['category'] == -1) {
4619 + $missing_fields[] = 'Category';
4621 + if ($data['priority'] == -1) {
4622 + $missing_fields[] = 'Priority';
4625 + // if there is no reporter set, use the system user
4626 + if (empty($data['reporter'])) {
4627 + $data['reporter'] = APP_SYSTEM_USER_ID;
4630 + if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
4631 + $data['estimated_dev_time'] = 0;
4635 + $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
4637 + "iss_prj_id=" . $prj_id . ",";
4638 + if (!empty($data['group'])) {
4639 + $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
4641 + if (!empty($data['category'])) {
4642 + $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
4644 + if (!empty($data['release'])) {
4645 + $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
4647 + if (!empty($data['priority'])) {
4648 + $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
4651 + $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
4653 + $initial_status = Project::getInitialStatus($prj_id);
4654 + if (!empty($initial_status)) {
4655 + $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
4658 + if (Customer::hasCustomerIntegration($prj_id)) {
4660 + iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
4661 + if (!empty($data['contact'])) {
4663 + iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
4666 + iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
4667 + iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
4668 + iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
4669 + iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
4670 + iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
4671 + iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
4675 + iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
4676 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
4677 + iss_last_public_action_type='created',
4678 + iss_summary='" . Misc::escapeString($data['summary']) . "',
4679 + iss_description='" . Misc::escapeString($data['description']) . "',
4680 + iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
4681 + if (!empty($data['contact'])) {
4683 + iss_private=" . Misc::escapeInteger($data['private']) . " ,";
4686 + iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
4689 + $res = DB_Helper::getInstance()->query($stmt);
4690 + if (PEAR::isError($res)) {
4691 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4695 + $issue_id = DB_Helper::get_last_insert_id();
4701 + * Method used to get a specific parameter in the issue listing cookie.
4704 + * @param string $name The name of the parameter
4705 + * @return mixed The value of the specified parameter
4707 + function getParam($name)
4709 + $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
4711 + if (isset($_GET[$name])) {
4712 + return $_GET[$name];
4713 + } elseif (isset($_POST[$name])) {
4714 + return $_POST[$name];
4715 + } elseif (isset($profile[$name])) {
4716 + return $profile[$name];
4724 + * Method used to save the current search parameters in a cookie.
4727 + * @return array The search parameters
4729 + function saveSearchParams()
4731 + $sort_by = self::getParam('sort_by');
4732 + $sort_order = self::getParam('sort_order');
4733 + $rows = self::getParam('rows');
4734 + $hide_closed = self::getParam('hide_closed');
4735 + if ($hide_closed === '') {
4738 + $search_type = self::getParam('search_type');
4739 + if (empty($search_type)) {
4740 + $search_type = 'all_text';
4742 + $custom_field = self::getParam('custom_field');
4743 + if (is_string($custom_field)) {
4744 + $custom_field = unserialize(urldecode($custom_field));
4747 + 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
4748 + 'pagerRow' => self::getParam('pagerRow'),
4749 + 'hide_closed' => $hide_closed,
4750 + "sort_by" => $sort_by ? $sort_by : "pri_rank",
4751 + "sort_order" => $sort_order ? $sort_order : "ASC",
4752 + // quick filter form
4753 + 'keywords' => self::getParam('keywords'),
4754 + 'search_type' => $search_type,
4755 + 'users' => self::getParam('users'),
4756 + 'status' => self::getParam('status'),
4757 + 'priority' => self::getParam('priority'),
4758 + 'category' => self::getParam('category'),
4759 + 'customer_email' => self::getParam('customer_email'),
4760 + // advanced search form
4761 + 'show_authorized_issues' => self::getParam('show_authorized_issues'),
4762 + 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
4763 + 'reporter' => self::getParam('reporter'),
4765 + 'release' => self::getParam('release'),
4767 + 'custom_field' => $custom_field
4769 + // now do some magic to properly format the date fields
4770 + $date_fields = array(
4773 + 'last_response_date',
4774 + 'first_response_date',
4777 + foreach ($date_fields as $field_name) {
4778 + $field = self::getParam($field_name);
4779 + if (empty($field)) {
4782 + if (@$field['filter_type'] == 'in_past') {
4783 + @$cookie[$field_name] = array(
4784 + 'filter_type' => 'in_past',
4785 + 'time_period' => $field['time_period']
4788 + $end_field_name = $field_name . '_end';
4789 + $end_field = self::getParam($end_field_name);
4790 + @$cookie[$field_name] = array(
4791 + 'past_hour' => $field['past_hour'],
4792 + 'Year' => $field['Year'],
4793 + 'Month' => $field['Month'],
4794 + 'Day' => $field['Day'],
4795 + 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
4796 + 'filter_type' => $field['filter_type'],
4797 + 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
4799 + @$cookie[$end_field_name] = array(
4800 + 'Year' => $end_field['Year'],
4801 + 'Month' => $end_field['Month'],
4802 + 'Day' => $end_field['Day']
4806 + Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
4812 + * Method used to get the current sorting options used in the grid layout
4813 + * of the issue listing page.
4816 + * @param array $options The current search parameters
4817 + * @return array The sorting options
4819 + function getSortingInfo($options)
4822 + $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
4824 + // default order for last action date, priority should be descending
4825 + // for textual fields, like summary, ascending is reasonable
4827 + "pri_rank" => "desc",
4828 + "iss_id" => "desc",
4829 + "iss_customer_id" => "desc",
4830 + "prc_title" => "asc",
4831 + "sta_rank" => "asc",
4832 + "iss_created_date" => "desc",
4833 + "iss_summary" => "asc",
4834 + "last_action_date" => "desc",
4835 + "usr_full_name" => "asc",
4836 + "iss_expected_resolution_date" => "desc",
4837 + "pre_title" => "asc",
4838 + "assigned" => "asc",
4841 + foreach ($custom_fields as $fld_id => $fld_name) {
4842 + $fields['custom_field_' . $fld_id] = "desc";
4845 + $sortfields = array_combine(array_keys($fields), array_keys($fields));
4846 + $sortfields["pre_title"] = "pre_scheduled_date";
4847 + $sortfields["assigned"] = "isu_usr_id";
4850 + "links" => array(),
4851 + "images" => array()
4853 + foreach ($sortfields as $field => $sortfield) {
4854 + $sort_order = $fields[$field];
4855 + if ($options["sort_by"] == $sortfield) {
4856 + $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
4857 + if (strtolower($options["sort_order"]) == "asc") {
4858 + $sort_order = "desc";
4860 + $sort_order = "asc";
4863 + $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
4870 + * Returns the list of action date fields appropriate for the
4871 + * current user ID.
4874 + * @return array The list of action date fields
4876 + function getLastActionFields()
4878 + $last_action_fields = array(
4879 + "iss_last_public_action_date"
4881 + if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
4882 + $last_action_fields[] = "iss_last_internal_action_date";
4884 + if (count($last_action_fields) > 1) {
4885 + return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
4887 + return $last_action_fields[0] . " AS last_action_date";
4893 + * Method used to get the list of issues to be displayed in the grid layout.
4896 + * @param integer $prj_id The current project ID
4897 + * @param array $options The search parameters
4898 + * @param integer $current_row The current page number
4899 + * @param integer $max The maximum number of rows per page
4900 + * @return array The list of issues to be displayed
4902 + function getListing($prj_id, $options, $current_row = 0, $max = 5)
4904 + if (strtoupper($max) == "ALL") {
4907 + $start = $current_row * $max;
4908 + // get the current user's role
4909 + $usr_id = Auth::getUserID();
4910 + $role_id = User::getRoleByUser($usr_id, $prj_id);
4912 + // get any custom fields that should be displayed
4913 + $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
4921 + iss_customer_contract_id,
4924 + iss_last_response_date,
4926 + iss_last_customer_action_date,
4932 + sta_color status_color,
4937 + iss_last_public_action_date,
4938 + iss_last_public_action_type,
4939 + iss_last_internal_action_date,
4940 + iss_last_internal_action_type,
4941 + " . self::getLastActionFields() . ",
4942 + IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
4945 + iss_percent_complete,
4947 + iss_expected_resolution_date
4950 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
4951 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
4952 + // join custom fields if we are searching by custom fields
4953 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
4954 + foreach ($options['custom_field'] as $fld_id => $search_value) {
4955 + if (empty($search_value)) {
4958 + $field = Custom_Field::getDetails($fld_id);
4959 + if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
4962 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
4965 + if ($field['fld_type'] == 'multiple') {
4966 + $search_value = Misc::escapeInteger($search_value);
4967 + foreach ($search_value as $cfo_id) {
4968 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
4971 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
4976 + // check for the custom fields we want to sort by
4977 + if (strstr($options['sort_by'], 'custom_field') !== false) {
4978 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
4979 + $stmt .= "\n LEFT JOIN \n" .
4980 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
4982 + (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
4984 + if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
4987 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4989 + isu_iss_id=iss_id";
4991 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
4994 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
4996 + iur_iss_id=iss_id";
4998 + if (!empty($options["show_notification_list_issues"])) {
5001 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
5003 + sub_iss_id=iss_id";
5007 + " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
5011 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5015 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5017 + iss_pre_id = pre_id
5019 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5023 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5027 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
5029 + iss_id=iqu_iss_id AND
5030 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
5032 + iss_prj_id= " . Misc::escapeInteger($prj_id);
5033 + $stmt .= self::buildWhereClause($options);
5035 + if (strstr($options["sort_by"], 'custom_field') !== false) {
5036 + $fld_details = Custom_Field::getDetails($fld_id);
5037 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
5039 + $sort_by = Misc::escapeString($options["sort_by"]);
5046 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
5048 + $total_rows = Pager::getTotalRows($stmt);
5051 + " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
5052 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
5053 + if (PEAR::isError($res)) {
5054 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5060 + if (count($res) > 0) {
5061 + self::getAssignedUsersByIssues($res);
5062 + Time_Tracking::getTimeSpentByIssues($res);
5063 + // need to get the customer titles for all of these issues...
5064 + if (Customer::hasCustomerIntegration($prj_id)) {
5065 + Customer::getCustomerTitlesByIssues($prj_id, $res);
5066 + Customer::getSupportLevelsByIssues($prj_id, $res);
5068 + self::formatLastActionDates($res);
5069 + self::getLastStatusChangeDates($prj_id, $res);
5070 + } elseif ($current_row > 0) {
5071 + // if there are no results, and the page is not the first page reset page to one and reload results
5072 + Auth::redirect("list.php?pagerRow=0&rows=$max");
5074 + $groups = Group::getAssocList($prj_id);
5075 + $categories = Category::getAssocList($prj_id);
5076 + $column_headings = self::getColumnHeadings($prj_id);
5077 + if (count($custom_fields) > 0) {
5078 + $column_headings = array_merge($column_headings,$custom_fields);
5080 + $csv[] = @implode("\t", $column_headings);
5081 + for ($i = 0; $i < count($res); $i++) {
5082 + $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
5083 + $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
5084 + $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
5086 + $res[$i]['pri_title'],
5087 + $res[$i]['iss_id'],
5088 + $res[$i]['usr_full_name'],
5090 + // hide the group column from the output if no
5091 + // groups are available in the database
5092 + if (count($groups) > 0) {
5093 + $fields[] = $res[$i]['group'];
5095 + $fields[] = $res[$i]['assigned_users'];
5096 + $fields[] = $res[$i]['time_spent'];
5097 + // hide the category column from the output if no
5098 + // categories are available in the database
5099 + if (count($categories) > 0) {
5100 + $fields[] = $res[$i]['prc_title'];
5102 + if (Customer::hasCustomerIntegration($prj_id)) {
5103 + $fields[] = @$res[$i]['customer_title'];
5104 + // check if current user is acustomer and has a per incident contract.
5105 + // if so, check if issue is redeemed.
5106 + if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
5107 + if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
5108 + (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
5109 + $res[$i]['redeemed'] = true;
5113 + $fields[] = $res[$i]['sta_title'];
5114 + $fields[] = $res[$i]["status_change_date"];
5115 + $fields[] = $res[$i]["last_action_date"];
5116 + $fields[] = $res[$i]['iss_dev_time'];
5117 + $fields[] = $res[$i]['iss_summary'];
5118 + $fields[] = $res[$i]['iss_expected_resolution_date'];
5120 + if (count($custom_fields) > 0) {
5121 + $res[$i]['custom_field'] = array();
5122 + $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
5123 + foreach ($custom_field_values as $this_field) {
5124 + if (!empty($custom_fields[$this_field['fld_id']])) {
5125 + $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
5126 + $fields[] = $this_field['value'];
5131 + $csv[] = @implode("\t", $fields);
5133 + $total_pages = ceil($total_rows / $max);
5134 + $last_page = $total_pages - 1;
5138 + "current_page" => $current_row,
5139 + "start_offset" => $start,
5140 + "end_offset" => $start + count($res),
5141 + "total_rows" => $total_rows,
5142 + "total_pages" => $total_pages,
5143 + "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
5144 + "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
5145 + "last_page" => $last_page,
5146 + "custom_fields" => $custom_fields
5148 + "csv" => @implode("\n", $csv)
5155 + * Processes a result set to format the "Last Action Date" column.
5158 + * @param array $result The result set
5160 + function formatLastActionDates(&$result)
5162 + for ($i = 0; $i < count($result); $i++) {
5163 + if (($result[$i]['action_type'] == "internal") &&
5164 + (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
5165 + $label = $result[$i]["iss_last_internal_action_type"];
5166 + $last_date = $result[$i]["iss_last_internal_action_date"];
5168 + $label = $result[$i]["iss_last_public_action_type"];
5169 + $last_date = $result[$i]["iss_last_public_action_date"];
5171 + $date = new Date($last_date);
5172 + $current = new Date(Date_Helper::getCurrentDateGMT());
5173 + $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
5174 + Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
5180 + * Retrieves the last status change date for the given issue.
5183 + * @param integer $prj_id The project ID
5184 + * @param array $result The associative array of data
5185 + * @see self::getListing()
5187 + function getLastStatusChangeDates($prj_id, &$result)
5190 + for ($i = 0; $i < count($result); $i++) {
5191 + $ids[] = $result[$i]["iss_sta_id"];
5193 + if (count($ids) == 0) {
5196 + $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
5197 + for ($i = 0; $i < count($result); $i++) {
5198 + if (empty($result[$i]['iss_sta_id'])) {
5199 + $result[$i]['status_change_date'] = '';
5201 + list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
5202 + if ((empty($label)) || (empty($date_field_name))) {
5203 + $result[$i]['status_change_date'] = '';
5206 + $current = new Date(Date_Helper::getCurrentDateGMT());
5207 + $desc = "$label: %s ago";
5208 + $target_date = $result[$i][$date_field_name];
5209 + if (empty($target_date)) {
5210 + $result[$i]['status_change_date'] = '';
5213 + $date = new Date($target_date);
5214 + $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
5221 + * Method used to get the list of issues to be displayed in the grid layout.
5224 + * @param array $options The search parameters
5225 + * @return string The where clause
5227 + function buildWhereClause($options)
5229 + $usr_id = Auth::getUserID();
5230 + $prj_id = Auth::getCurrentProject();
5231 + $role_id = User::getRoleByUser($usr_id, $prj_id);
5233 + $stmt = ' AND iss_usr_id = usr_id';
5234 + if ($role_id == User::getRoleID('Customer')) {
5235 + $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
5236 + } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
5238 + iss_usr_id = $usr_id OR
5239 + iur_usr_id = $usr_id
5243 + if (!empty($options["users"])) {
5244 + $stmt .= " AND (\n";
5245 + if (stristr($options["users"], "grp") !== false) {
5246 + $chunks = explode(":", $options["users"]);
5247 + $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
5249 + if ($options['users'] == '-1') {
5250 + $stmt .= 'isu_usr_id IS NULL';
5251 + } elseif ($options['users'] == '-2') {
5252 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
5253 + } elseif ($options['users'] == '-3') {
5254 + $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
5255 + } elseif ($options['users'] == '-4') {
5256 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
5258 + $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
5263 + if (!empty($options["reporter"])) {
5264 + $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
5266 + if (!empty($options["show_authorized_issues"])) {
5267 + $stmt .= " AND (iur_usr_id=$usr_id)";
5269 + if (!empty($options["show_notification_list_issues"])) {
5270 + $stmt .= " AND (sub_usr_id=$usr_id)";
5272 + if (!empty($options["keywords"])) {
5273 + $stmt .= " AND (\n";
5274 + if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
5275 + $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
5276 + } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
5277 + // check if the user is trying to search by customer email
5278 + $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
5279 + if (count($customer_ids) > 0) {
5280 + $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
5282 + // no results, kill query
5283 + $stmt .= " iss_customer_id = -1";
5286 + $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
5287 + $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
5291 + if (!empty($options["priority"])) {
5292 + $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
5294 + if (!empty($options["status"])) {
5295 + $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
5297 + if (!empty($options["category"])) {
5298 + if (!is_array($options['category'])) {
5299 + $options['category'] = array($options['category']);
5301 + $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
5303 + if (!empty($options["hide_closed"])) {
5304 + $stmt .= " AND sta_is_closed=0";
5306 + if (!empty($options['release'])) {
5307 + $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
5309 + // now for the date fields
5310 + $date_fields = array(
5313 + 'last_response_date',
5314 + 'first_response_date',
5317 + foreach ($date_fields as $field_name) {
5318 + if (!empty($options[$field_name])) {
5319 + switch ($options[$field_name]['filter_type']) {
5321 + $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
5324 + $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
5327 + $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
5330 + $stmt .= " AND iss_$field_name IS NULL";
5333 + if (strlen($options[$field_name]['time_period']) == 0) {
5334 + $options[$field_name]['time_period'] = 0;
5336 + $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
5337 + Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
5343 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
5344 + foreach ($options['custom_field'] as $fld_id => $search_value) {
5345 + if (empty($search_value)) {
5348 + $field = Custom_Field::getDetails($fld_id);
5349 + $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
5350 + if (($field['fld_type'] == 'date') &&
5351 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
5354 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
5358 + if ($field['fld_type'] == 'multiple') {
5359 + $search_value = Misc::escapeInteger($search_value);
5360 + foreach ($search_value as $cfo_id) {
5361 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
5362 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
5363 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
5365 + } elseif ($field['fld_type'] == 'date') {
5366 + if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
5369 + $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
5370 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
5371 + cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
5372 + } else if ($field['fld_type'] == 'integer') {
5373 + $value = $search_value['value'];
5374 + switch ($search_value['filter_type']) {
5375 + case 'ge': $cmp = '>='; break;
5376 + case 'le': $cmp = '<='; break;
5377 + case 'gt': $cmp = '>'; break;
5378 + case 'lt': $cmp = '<'; break;
5379 + default: $cmp = '='; break;
5381 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
5382 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
5383 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
5385 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
5386 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
5387 + if ($field['fld_type'] == 'combo') {
5388 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
5390 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
5396 + // clear cached full-text values if we are not searching fulltext anymore
5397 + if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
5398 + Session::set('fulltext_string', '');
5399 + Session::set('fulltext_issues', '');
5406 + * Method used to get the previous and next issues that are available
5407 + * according to the current search parameters.
5410 + * @param integer $issue_id The issue ID
5411 + * @param array $options The search parameters
5412 + * @return array The list of issues
5414 + function getSides($issue_id, $options)
5416 + $usr_id = Auth::getUserID();
5417 + $role_id = Auth::getCurrentRole();
5421 + " . self::getLastActionFields() . "
5424 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5425 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
5426 + // join custom fields if we are searching by custom fields
5427 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
5428 + foreach ($options['custom_field'] as $fld_id => $search_value) {
5429 + if (empty($search_value)) {
5432 + $field = Custom_Field::getDetails($fld_id);
5433 + if (($field['fld_type'] == 'date') &&
5434 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
5437 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
5441 + if ($field['fld_type'] == 'multiple') {
5442 + $search_value = Misc::escapeInteger($search_value);
5443 + foreach ($search_value as $cfo_id) {
5444 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
5447 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
5452 + // check for the custom fields we want to sort by
5453 + if (strstr($options['sort_by'], 'custom_field') !== false) {
5454 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
5455 + $stmt .= "\n LEFT JOIN \n" .
5456 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
5458 + (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
5460 + if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
5463 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
5465 + isu_iss_id=iss_id";
5467 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
5470 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
5472 + iur_iss_id=iss_id";
5474 + if (!empty($options["show_notification_list_issues"])) {
5477 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
5479 + sub_iss_id=iss_id";
5481 + if (@$options["sort_by"] == "pre_scheduled_date") {
5484 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5486 + iss_pre_id = pre_id";
5488 + if (@$options['sort_by'] == 'prc_title') {
5491 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5493 + iss_prc_id = prc_id";
5497 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5501 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5505 + iss_prj_id=" . Auth::getCurrentProject();
5506 + $stmt .= self::buildWhereClause($options);
5507 + if (strstr($options["sort_by"], 'custom_field') !== false) {
5508 + $fld_details = Custom_Field::getDetails($fld_id);
5509 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
5511 + $sort_by = Misc::escapeString($options["sort_by"]);
5517 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
5519 + $res = DB_Helper::getInstance()->getCol($stmt);
5520 + if (PEAR::isError($res)) {
5521 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5524 + // COMPAT: the next line requires PHP >= 4.0.5
5525 + $index = array_search($issue_id, $res);
5526 + if (!empty($res[$index+1])) {
5527 + $next = $res[$index+1];
5529 + if (!empty($res[$index-1])) {
5530 + $previous = $res[$index-1];
5534 + "previous" => @$previous
5541 + * Method used to get the full list of user IDs assigned to a specific
5545 + * @param integer $issue_id The issue ID
5546 + * @return array The list of user IDs
5548 + function getAssignedUserIDs($issue_id)
5553 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5554 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5556 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
5557 + isu_usr_id=usr_id";
5558 + $res = DB_Helper::getInstance()->getCol($stmt);
5559 + if (PEAR::isError($res)) {
5560 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5569 + * Method used to see if a user is assigned to an issue.
5572 + * @param integer $issue_id The issue ID
5573 + * @param integer $usr_id An integer containg the ID of the user.
5574 + * @return boolean true if the user(s) are assigned to the issue.
5576 + function isAssignedToUser($issue_id, $usr_id)
5578 + $assigned_users = self::getAssignedUserIDs($issue_id);
5579 + if (in_array($usr_id, $assigned_users)) {
5588 + * Method used to get the full list of reporters associated with a given
5592 + * @param array $result The result set
5595 + function getReportersByIssues(&$result)
5598 + for ($i = 0; $i < count($result); $i++) {
5599 + $ids[] = $result[$i]["iss_id"];
5601 + $ids = implode(", ", $ids);
5604 + CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
5606 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5607 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5609 + iss_usr_id=usr_id AND
5610 + iss_id IN ($ids)";
5611 + $res = DB_Helper::getInstance()->getAssoc($stmt);
5612 + if (PEAR::isError($res)) {
5613 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5615 + // now populate the $result variable again
5616 + for ($i = 0; $i < count($result); $i++) {
5617 + @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
5624 + * Method used to get the full list of assigned users by a list
5625 + * of issues. This was originally created to optimize the issue
5629 + * @param array $result The result set
5632 + function getAssignedUsersByIssues(&$result)
5635 + for ($i = 0; $i < count($result); $i++) {
5636 + $ids[] = $result[$i]["iss_id"];
5638 + if (count($ids) < 1) {
5641 + $ids = implode(", ", $ids);
5646 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5647 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5649 + isu_usr_id=usr_id AND
5650 + isu_iss_id IN ($ids)";
5651 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
5652 + if (PEAR::isError($res)) {
5653 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5656 + for ($i = 0; $i < count($res); $i++) {
5657 + if (!empty($t[$res[$i]['isu_iss_id']])) {
5658 + $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
5660 + $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
5663 + // now populate the $result variable again
5664 + for ($i = 0; $i < count($result); $i++) {
5665 + @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
5672 + * Method used to add the issue description to a list of issues.
5675 + * @param array $result The result set
5678 + function getDescriptionByIssues(&$result)
5680 + if (count($result) == 0) {
5685 + for ($i = 0; $i < count($result); $i++) {
5686 + $ids[] = $result[$i]["iss_id"];
5688 + $ids = implode(", ", $ids);
5694 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
5696 + iss_id in ($ids)";
5697 + $res = DB_Helper::getInstance()->getAssoc($stmt);
5698 + if (PEAR::isError($res)) {
5699 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5701 + for ($i = 0; $i < count($result); $i++) {
5702 + @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
5709 + * Method used to get the full list of users (the full names) assigned to a
5713 + * @param integer $issue_id The issue ID
5714 + * @return array The list of users
5716 + function getAssignedUsers($issue_id)
5721 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5722 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5724 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
5725 + isu_usr_id=usr_id";
5726 + $res = DB_Helper::getInstance()->getCol($stmt);
5727 + if (PEAR::isError($res)) {
5728 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5737 + * Method used to get the details for a specific issue.
5740 + * @param integer $issue_id The issue ID
5741 + * @param boolean $force_refresh If the cache should not be used.
5742 + * @return array The details for the specified issue
5744 + function getDetails($issue_id, $force_refresh = false)
5748 + $issue_id = Misc::escapeInteger($issue_id);
5750 + if (empty($issue_id)) {
5754 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
5755 + return $returns[$issue_id];
5759 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
5766 + sta_color status_color,
5770 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5771 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
5774 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5778 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5782 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5786 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5790 + iss_id=$issue_id AND
5791 + iss_prj_id=prj_id";
5792 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
5793 + if (PEAR::isError($res)) {
5794 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5797 + if (empty($res)) {
5800 + $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
5801 + // get customer information, if any
5802 + if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
5803 + $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
5804 + $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
5805 + $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
5806 + $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
5807 + $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
5808 + $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
5809 + if (empty($res['iss_first_response_date'])) {
5810 + $first_response_deadline = $created_date_ts + $max_first_response_time;
5811 + if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
5812 + $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
5814 + $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
5818 + $res['iss_original_description'] = $res["iss_description"];
5819 + if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
5820 + $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
5821 + $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
5823 + $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
5824 + $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
5825 + $res['iss_created_date_ts'] = $created_date_ts;
5826 + $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
5827 + list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
5828 + $temp = self::getAssignedUsersStatus($res["iss_id"]);
5829 + $res["has_inactive_users"] = 0;
5830 + $res["assigned_users"] = array();
5831 + $res["assigned_inactive_users"] = array();
5832 + foreach ($temp as $usr_id => $usr_status) {
5833 + if (!User::isActiveStatus($usr_status)) {
5834 + $res["assigned_inactive_users"][] = $usr_id;
5835 + $res["has_inactive_users"] = 1;
5837 + $res["assigned_users"][] = $usr_id;
5840 + if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
5841 + $res["is_current_user_assigned"] = 1;
5843 + $res["is_current_user_assigned"] = 0;
5845 + $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
5846 + $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
5847 + $res["reporter"] = User::getFullName($res["iss_usr_id"]);
5848 + if (empty($res["iss_updated_date"])) {
5849 + $res["iss_updated_date"] = 'not updated yet';
5851 + $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
5853 + $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
5854 + if (Release::isAssignable($res["iss_pre_id"])) {
5855 + $release = Release::getDetails($res["iss_pre_id"]);
5856 + $res["pre_title"] = $release["pre_title"];
5857 + $res["pre_status"] = $release["pre_status"];
5859 + // need to return the list of issues that are duplicates of this one
5860 + $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
5861 + $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
5862 + // also get the issue title of the duplicated issue
5863 + if (!empty($res['iss_duplicated_iss_id'])) {
5864 + $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
5867 + // get group information
5868 + if (!empty($res["iss_grp_id"])) {
5869 + $res["group"] = Group::getDetails($res["iss_grp_id"]);
5872 + // get quarantine issue
5873 + $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
5875 + $returns[$issue_id] = $res;
5883 + * Method used to get some simple details about the given duplicated issue.
5886 + * @param integer $issue_id The issue ID
5887 + * @return array The duplicated issue details
5889 + function getDuplicatedDetails($issue_id)
5892 + iss_summary title,
5893 + sta_title current_status,
5894 + sta_is_closed is_closed
5896 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5897 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5899 + iss_sta_id=sta_id AND
5900 + iss_id=$issue_id";
5901 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
5902 + if (PEAR::isError($res)) {
5903 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5912 + * Method used to bulk update a list of issues
5917 + function bulkUpdate()
5919 + // check if user performing this chance has the proper role
5920 + if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
5924 + $items = Misc::escapeInteger($_POST['item']);
5925 + $new_status_id = Misc::escapeInteger($_POST['status']);
5926 + $new_release_id = Misc::escapeInteger(@$_POST['release']);
5927 + $new_priority_id = Misc::escapeInteger($_POST['priority']);
5928 + $new_category_id = Misc::escapeInteger($_POST['category']);
5930 + for ($i = 0; $i < count($items); $i++) {
5931 + if (!self::canAccess($items[$i], Auth::getUserID())) {
5933 + } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
5934 + // make sure issue is not in another project
5938 + $updated_fields = array();
5940 + // update assignment
5941 + if (count(@$_POST['users']) > 0) {
5942 + $users = Misc::escapeInteger($_POST['users']);
5943 + // get who this issue is currently assigned too
5948 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5949 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5951 + isu_usr_id = usr_id AND
5952 + isu_iss_id = " . $items[$i];
5953 + $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
5954 + if (PEAR::isError($current_assignees)) {
5955 + Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
5958 + foreach ($current_assignees as $usr_id => $usr_name) {
5959 + if (!in_array($usr_id, $users)) {
5960 + self::deleteUserAssociation($items[$i], $usr_id, false);
5963 + $new_user_names = array();
5964 + $new_assignees = array();
5965 + foreach ($users as $usr_id) {
5966 + $new_user_names[$usr_id] = User::getFullName($usr_id);
5968 + // check if the issue is already assigned to this person
5972 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
5974 + isu_iss_id=" . $items[$i] . " AND
5975 + isu_usr_id=" . $usr_id;
5976 + $total = DB_Helper::getInstance()->getOne($stmt);
5980 + $new_assignees[] = $usr_id;
5981 + // add the assignment
5982 + self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
5983 + Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
5984 + Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
5987 + Notification::notifyNewAssignment($new_assignees, $items[$i]);
5988 + $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
5992 + if (!empty($new_status_id)) {
5993 + $old_status_id = self::getStatusID($items[$i]);
5994 + $res = self::setStatus($items[$i], $new_status_id, false);
5996 + $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
6001 + if (!empty($new_release_id)) {
6002 + $old_release_id = self::getRelease($items[$i]);
6003 + $res = self::setRelease($items[$i], $new_release_id);
6005 + $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
6009 + // update priority
6010 + if (!empty($new_priority_id)) {
6011 + $old_priority_id = self::getPriority($items[$i]);
6012 + $res = self::setPriority($items[$i], $new_priority_id);
6014 + $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
6018 + // update category
6019 + if (!empty($new_category_id)) {
6020 + $old_category_id = self::getCategory($items[$i]);
6021 + $res = self::setCategory($items[$i], $new_category_id);
6023 + $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
6027 + if (count($updated_fields) > 0) {
6028 + // log the changes
6031 + foreach ($updated_fields as $key => $value) {
6035 + $changes .= "$key: $value";
6038 + History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
6041 + // close if request
6042 + if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
6043 + self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
6051 + * Method used to set the initial impact analysis for a specific issue
6054 + * @param integer $issue_id The issue ID
6055 + * @return integer 1 if the update worked, -1 otherwise
6057 + function setImpactAnalysis($issue_id)
6060 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6062 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
6063 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6064 + iss_last_internal_action_type='update',
6065 + iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
6066 + iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
6068 + iss_id=" . Misc::escapeInteger($issue_id);
6069 + $res = DB_Helper::getInstance()->query($stmt);
6070 + if (PEAR::isError($res)) {
6071 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6074 + // add the impact analysis to the history of the issue
6075 + $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
6076 + History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
6083 + * Method used to get the full list of issue IDs that area available in the
6087 + * @param string $extra_condition An extra condition in the WHERE clause
6088 + * @return array The list of issue IDs
6090 + function getColList($extra_condition = NULL)
6095 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6097 + iss_prj_id=" . Auth::getCurrentProject();
6098 + if (!empty($extra_condition)) {
6099 + $stmt .= " AND $extra_condition ";
6104 + $res = DB_Helper::getInstance()->getCol($stmt);
6105 + if (PEAR::isError($res)) {
6106 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6115 + * Method used to get the full list of issue IDs and their respective
6119 + * @param string $extra_condition An extra condition in the WHERE clause
6120 + * @return array The list of issues
6122 + function getAssocList($extra_condition = NULL)
6128 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6130 + iss_prj_id=" . Auth::getCurrentProject();
6131 + if (!empty($extra_condition)) {
6132 + $stmt .= " AND $extra_condition ";
6137 + $res = DB_Helper::getInstance()->getAssoc($stmt);
6138 + if (PEAR::isError($res)) {
6139 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6148 + * Method used to get the list of issues associated to a specific issue.
6151 + * @param integer $issue_id The issue ID
6152 + * @return array The list of associated issues
6154 + function getAssociatedIssues($issue_id)
6156 + $issues = self::getAssociatedIssuesDetails($issue_id);
6157 + $associated = array();
6158 + for ($i = 0; $i < count($issues); $i++) {
6159 + $associated[] = $issues[$i]['associated_issue'];
6161 + return $associated;
6166 + * Method used to get the list of issues associated details to a
6170 + * @param integer $issue_id The issue ID
6171 + * @return array The list of associated issues
6173 + function getAssociatedIssuesDetails($issue_id)
6177 + if (!empty($returns[$issue_id])) {
6178 + return $returns[$issue_id];
6182 + isa_associated_id associated_issue,
6183 + iss_summary associated_title,
6184 + sta_title current_status,
6185 + sta_is_closed is_closed
6187 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
6188 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6189 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
6191 + isa_associated_id=iss_id AND
6192 + iss_sta_id=sta_id AND
6193 + isa_issue_id=$issue_id";
6194 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
6195 + if (PEAR::isError($res)) {
6196 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6199 + $returns[$issue_id] = $res;
6206 + * Method used to check whether an issue was already closed or not.
6209 + * @param integer $issue_id The issue ID
6212 + function isClosed($issue_id)
6217 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6218 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
6220 + iss_id=" . Misc::escapeInteger($issue_id) . " AND
6221 + iss_sta_id=sta_id AND
6223 + $res = DB_Helper::getInstance()->getOne($stmt);
6224 + if (PEAR::isError($res)) {
6225 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6238 + * Returns a simple list of issues that are currently set to some
6239 + * form of quarantine. This is mainly used by the IRC interface.
6242 + * @return array List of quarantined issues
6244 + function getQuarantinedIssueList()
6246 + // XXX: would be nice to restrict the result list to only one project
6251 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6252 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6254 + iqu_iss_id=iss_id AND
6255 + iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
6256 + iqu_expiration IS NOT NULL";
6257 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
6258 + if (PEAR::isError($res)) {
6259 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6262 + self::getAssignedUsersByIssues($res);
6269 + * Returns the status of a quarantine.
6271 + * @param integer $issue_id The issue ID
6272 + * @return integer Indicates what the current state of quarantine is.
6274 + function getQuarantineInfo($issue_id)
6280 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6282 + iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
6283 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
6284 + iqu_expiration IS NULL)";
6285 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
6286 + if (PEAR::isError($res)) {
6287 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6290 + if (!empty($res["iqu_expiration"])) {
6291 + $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
6292 + $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
6300 + * Sets the quarantine status. Optionally an expiration date can be set
6301 + * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
6304 + * @param integer $issue_id The issue ID
6305 + * @param integer $status The quarantine status
6306 + * @param string $expiration The expiration date of quarantine (default empty)
6308 + function setQuarantine($issue_id, $status, $expiration = '')
6310 + $issue_id = Misc::escapeInteger($issue_id);
6311 + $status = Misc::escapeInteger($status);
6313 + // see if there is an existing record
6317 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6319 + iqu_iss_id = $issue_id";
6320 + $res = DB_Helper::getInstance()->getOne($stmt);
6321 + if (PEAR::isError($res)) {
6322 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6328 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6330 + iqu_status = $status";
6331 + if (!empty($expiration)) {
6332 + $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
6335 + iqu_iss_id = $issue_id";
6336 + $res = DB_Helper::getInstance()->query($stmt);
6337 + if (PEAR::isError($res)) {
6338 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6341 + // add history entry about this change taking place
6342 + if ($status == 0) {
6343 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
6344 + "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
6349 + $stmt = "INSERT INTO
6350 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6354 + if (!empty($expiration)) {
6355 + $stmt .= ",\niqu_expiration\n";
6357 + $stmt .= ") VALUES (
6360 + if (!empty($expiration)) {
6361 + $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
6364 + $res = DB_Helper::getInstance()->query($stmt);
6365 + if (PEAR::isError($res)) {
6366 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6375 + * Sets the group of the issue.
6378 + * @param integer $issue_id The ID of the issue
6379 + * @param integer $group_id The ID of the group
6380 + * @return integer 1 if successful, -1 or -2 otherwise
6382 + function setGroup($issue_id, $group_id)
6384 + $issue_id = Misc::escapeInteger($issue_id);
6385 + $group_id = Misc::escapeInteger($group_id);
6387 + $current = self::getDetails($issue_id);
6388 + if ($current["iss_grp_id"] == $group_id) {
6392 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6394 + iss_grp_id = $group_id
6396 + iss_id = $issue_id";
6397 + $res = DB_Helper::getInstance()->query($stmt);
6398 + if (PEAR::isError($res)) {
6399 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6402 + $current_user = Auth::getUserID();
6403 + if (empty($current_user)) {
6404 + $current_user = APP_SYSTEM_USER_ID;
6406 + History::add($issue_id, $current_user, History::getTypeID('group_changed'),
6407 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
6413 + * Returns the group ID associated with the given issue ID.
6416 + * @param integer $issue_id The issue ID
6417 + * @return integer The associated group ID
6419 + function getGroupID($issue_id)
6424 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6426 + iss_id=" . Misc::escapeInteger($issue_id);
6427 + $res = DB_Helper::getInstance()->getOne($stmt);
6428 + if (PEAR::isError($res)) {
6429 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6438 + * Returns an array of issues based on full text search results.
6440 + * @param array $options An array of search options
6441 + * @return array An array of issue IDS
6443 + function getFullTextIssues($options)
6445 + // check if a list of issues for this full text search is already cached
6446 + $fulltext_string = Session::get('fulltext_string');
6447 + if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
6448 + return Session::get('fulltext_issues');
6451 + // no pre-existing list, generate them
6455 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6457 + MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6460 + DISTINCT(not_iss_id)
6462 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
6464 + MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6467 + DISTINCT(ttr_iss_id)
6469 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
6471 + MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6474 + DISTINCT(phs_iss_id)
6476 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
6478 + MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6481 + DISTINCT(sup_iss_id)
6483 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
6484 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
6486 + sup_id = seb_sup_id AND
6487 + MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6489 + $res = DB_Helper::getInstance()->getCol($stmt);
6490 + if (PEAR::isError($res)) {
6491 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6495 + DISTINCT(icf_iss_id)
6497 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
6499 + MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
6500 + $custom_res = DB_Helper::getInstance()->getCol($stmt);
6501 + if (PEAR::isError($custom_res)) {
6502 + Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
6505 + $issues = array_merge($res, $custom_res);
6506 + // we kill the query results on purpose to flag that no
6507 + // issues could be found with fulltext search
6508 + if (count($issues) < 1) {
6509 + $issues = array(-1);
6511 + Session::set('fulltext_string', $options['keywords']);
6512 + Session::set('fulltext_issues', $issues);
6519 + * Method to determine if user can access a particular issue
6522 + * @param integer $issue_id The ID of the issue.
6523 + * @param integer $usr_id The ID of the user
6524 + * @return boolean If the user can access the issue
6526 + function canAccess($issue_id, $usr_id)
6530 + if (empty($issue_id)) {
6534 + if (isset($access[$issue_id . "-" . $usr_id])) {
6535 + return $access[$issue_id . "-" . $usr_id];
6538 + $details = self::getDetails($issue_id);
6539 + if (empty($details)) {
6542 + $usr_details = User::getDetails($usr_id);
6543 + $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
6544 + $prj_id = self::getProjectID($issue_id);
6547 + if (empty($usr_role)) {
6548 + // check if they are even allowed to access the project
6550 + } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
6551 + ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
6552 + // check customer permissions
6554 + } elseif ($details['iss_private'] == 1) {
6555 + // check if the issue is even private
6557 + // check role, reporter, assigment and group
6558 + if ($usr_role > User::getRoleID("Developer")) {
6560 + } elseif ($details['iss_usr_id'] == $usr_id) {
6562 + } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
6564 + } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
6565 + ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
6567 + } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
6572 + } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
6573 + ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
6579 + $access[$issue_id . "-" . $usr_id] = $return;
6585 + * Returns true if the specified issue is private, false otherwise
6588 + * @param integer $issue_id The ID of the issue
6589 + * @return boolean If the issue is private or not
6591 + function isPrivate($issue_id)
6595 + if (!isset($returns[$issue_id])) {
6599 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6601 + iss_id=$issue_id";
6602 + $res = DB_Helper::getInstance()->getOne($sql);
6603 + if (PEAR::isError($res)) {
6604 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6608 + $returns[$issue_id] = true;
6610 + $returns[$issue_id] = false;
6614 + return $returns[$issue_id];
6619 + * Clears closed information from an issues.
6622 + * @param integer $issue_id The ID of the issue
6624 + function clearClosed($issue_id)
6627 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6629 + iss_closed_date = null,
6632 + iss_id=" . Misc::escapeInteger($issue_id);
6633 + $res = DB_Helper::getInstance()->query($stmt);
6634 + if (PEAR::isError($res)) {
6635 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6642 + * Returns the message ID that should be used as the parent ID for all messages
6645 + * @param integer $issue_id The ID of the issue
6647 + function getRootMessageID($issue_id)
6650 + iss_root_message_id
6652 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6654 + iss_id=" . Misc::escapeInteger($issue_id);
6655 + $res = DB_Helper::getInstance()->getOne($sql);
6656 + if (PEAR::isError($res)) {
6657 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6666 + * Returns the issue ID of the issue with the specified root message ID, or false
6668 + * @param string $msg_id The Message ID
6669 + * @return integer The ID of the issue
6671 + function getIssueByRootMessageID($msg_id)
6675 + if (!empty($returns[$msg_id])) {
6676 + return $returns[$msg_id];
6681 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6683 + iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
6684 + $res = DB_Helper::getInstance()->getOne($sql);
6685 + if (PEAR::isError($res)) {
6686 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6689 + if (empty($res)) {
6690 + $returns[$msg_id] = false;
6692 + $returns[$msg_id] = $res;
6694 + return $returns[$msg_id];
6699 + * Sets the assignees for the issue
6701 + * @param integer $issue_id
6702 + * @param array $assignees
6704 + function setAssignees($issue_id, $assignees)
6706 + if (!is_array($assignees)) {
6707 + $assignees = array();
6710 + // see if there is anything to change
6711 + $old_assignees = self::getAssignedUserIDs($issue_id);
6712 + if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
6716 + $old_assignee_names = self::getAssignedUsers($issue_id);
6718 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
6719 + // clear up the assignments for this issue, and then assign it to the current user
6720 + self::deleteUserAssociations($issue_id);
6721 + $assignee_names = array();
6722 + foreach ($assignees as $assignee) {
6723 + $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
6727 + $assignee_names[] = User::getFullName($assignee);
6728 + Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
6731 + Notification::notifyNewAssignment($assignees, $issue_id);
6733 + // save a history entry about this...
6734 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
6735 + "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
6738 --- eventum-2.2/lib/eventum/class.issue.php.~2~ 1970-01-01 02:00:00.000000000 +0200
6739 +++ eventum-2.2-order/lib/eventum/class.issue.php.~2~ 2009-10-12 22:10:36.432519927 +0300
6742 +/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
6743 +// +----------------------------------------------------------------------+
6744 +// | Eventum - Issue Tracking System |
6745 +// +----------------------------------------------------------------------+
6746 +// | Copyright (c) 2003 - 2008 MySQL AB |
6747 +// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
6749 +// | This program is free software; you can redistribute it and/or modify |
6750 +// | it under the terms of the GNU General Public License as published by |
6751 +// | the Free Software Foundation; either version 2 of the License, or |
6752 +// | (at your option) any later version. |
6754 +// | This program is distributed in the hope that it will be useful, |
6755 +// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
6756 +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
6757 +// | GNU General Public License for more details. |
6759 +// | You should have received a copy of the GNU General Public License |
6760 +// | along with this program; if not, write to: |
6762 +// | Free Software Foundation, Inc. |
6763 +// | 59 Temple Place - Suite 330 |
6764 +// | Boston, MA 02111-1307, USA. |
6765 +// +----------------------------------------------------------------------+
6766 +// | Authors: João Prado Maia <jpm@mysql.com> |
6767 +// +----------------------------------------------------------------------+
6772 + * Class designed to handle all business logic related to the issues in the
6773 + * system, such as adding or updating them or listing them in the grid mode.
6775 + * @author João Prado Maia <jpm@mysql.com>
6776 + * @version $Revision$
6782 + * Method used to check whether a given issue ID exists or not.
6785 + * @param integer $issue_id The issue ID
6786 + * @param boolean $check_project If we should check that this issue is in the current project
6789 + function exists($issue_id, $check_project = true)
6794 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6796 + iss_id=" . Misc::escapeInteger($issue_id);
6797 + if ($check_project) {
6799 + iss_prj_id = " . Auth::getCurrentProject();
6801 + $res = DB_Helper::getInstance()->getOne($stmt);
6802 + if (PEAR::isError($res)) {
6803 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6816 + * Method used to get the list of column heading titles for the
6817 + * CSV export functionality of the issue listing screen.
6820 + * @param integer $prj_id The project ID
6821 + * @return array The list of column heading titles
6823 + function getColumnHeadings($prj_id)
6825 + $headings = array(
6830 + // hide the group column from the output if no
6831 + // groups are available in the database
6832 + $groups = Group::getAssocList($prj_id);
6833 + if (count($groups) > 0) {
6834 + $headings[] = 'Group';
6836 + $headings[] = 'Assigned';
6837 + $headings[] = 'Time Spent';
6838 + // hide the category column from the output if no
6839 + // categories are available in the database
6840 + $categories = Category::getAssocList($prj_id);
6841 + if (count($categories) > 0) {
6842 + $headings[] = 'Category';
6844 + if (Customer::hasCustomerIntegration($prj_id)) {
6845 + $headings[] = 'Customer';
6847 + $headings[] = 'Status';
6848 + $headings[] = 'Status Change Date';
6849 + $headings[] = 'Last Action Date';
6850 + $headings[] = 'Est. Dev. TIme';
6851 + $headings[] = 'Summary';
6852 + $headings[] = 'Expected Resolution Date';
6858 + * Method used to get the full list of date fields available to issues, to
6859 + * be used when customizing the issue listing screen in the 'last status
6860 + * change date' column.
6863 + * @param boolean $display_customer_fields Whether to include any customer related fields or not
6864 + * @return array The list of available date fields
6866 + function getDateFieldsAssocList($display_customer_fields = FALSE)
6869 + 'iss_created_date' => 'Created Date',
6870 + 'iss_updated_date' => 'Last Updated Date',
6871 + 'iss_last_response_date' => 'Last Response Date',
6872 + 'iss_closed_date' => 'Closed Date'
6874 + if ($display_customer_fields) {
6875 + $fields['iss_last_customer_action_date'] = 'Customer Action Date';
6883 + * Method used to get the full list of issue IDs and their respective
6884 + * titles associated to a given project.
6887 + * @param integer $prj_id The project ID
6888 + * @return array The list of issues
6890 + function getAssocListByProject($prj_id)
6896 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6898 + iss_prj_id=" . Misc::escapeInteger($prj_id) . "
6901 + $res = DB_Helper::getInstance()->getAssoc($stmt);
6902 + if (PEAR::isError($res)) {
6903 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6912 + * Method used to get the status of a given issue.
6915 + * @param integer $issue_id The issue ID
6916 + * @return integer The status ID
6918 + function getStatusID($issue_id)
6922 + $issue_id = Misc::escapeInteger($issue_id);
6924 + if (!empty($returns[$issue_id])) {
6925 + return $returns[$issue_id];
6931 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6933 + iss_id=$issue_id";
6934 + $res = DB_Helper::getInstance()->getOne($stmt);
6935 + if (PEAR::isError($res)) {
6936 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6939 + $returns[$issue_id] = $res;
6946 + * Records the last customer action date for a given issue ID.
6949 + * @param integer $issue_id The issue ID
6950 + * @return integer 1 if the update worked, -1 otherwise
6952 + function recordLastCustomerAction($issue_id)
6955 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6957 + iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6958 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6959 + iss_last_public_action_type='customer action'
6961 + iss_id=" . Misc::escapeInteger($issue_id);
6962 + $res = DB_Helper::getInstance()->query($stmt);
6963 + if (PEAR::isError($res)) {
6964 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6973 + * Returns the customer ID associated with the given issue ID.
6976 + * @param integer $issue_id The issue ID
6977 + * @return integer The customer ID associated with the issue
6979 + function getCustomerID($issue_id)
6983 + $issue_id = Misc::escapeInteger($issue_id);
6985 + if (!empty($returns[$issue_id])) {
6986 + return $returns[$issue_id];
6992 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6994 + iss_id=$issue_id";
6995 + $res = DB_Helper::getInstance()->getOne($stmt);
6996 + if (PEAR::isError($res)) {
6997 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7000 + $returns[$issue_id] = $res;
7007 + * Returns the contract ID associated with the given issue ID.
7010 + * @param integer $issue_id The issue ID
7011 + * @return integer The customer ID associated with the issue
7013 + function getContractID($issue_id)
7017 + $issue_id = Misc::escapeInteger($issue_id);
7019 + if (!empty($returns[$issue_id])) {
7020 + return $returns[$issue_id];
7024 + iss_customer_contract_id
7026 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7028 + iss_id=$issue_id";
7029 + $res = DB_Helper::getInstance()->getOne($stmt);
7030 + if (PEAR::isError($res)) {
7031 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7034 + $returns[$issue_id] = $res;
7041 + * Sets the contract ID for a specific issue.
7044 + * @param integer $issue_id The issue ID
7045 + * @param integer The contract ID
7046 + * @return integer 1 if the update worked, -1 otherwise
7048 + function setContractID($issue_id, $contract_id)
7050 + $issue_id = Misc::escapeInteger($issue_id);
7052 + $old_contract_id = self::getContractID($issue_id);
7055 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7057 + iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
7059 + iss_id=$issue_id";
7060 + $res = DB_Helper::getInstance()->query($stmt);
7061 + if (PEAR::isError($res)) {
7062 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7066 + History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
7073 + * Returns the customer ID associated with the given issue ID.
7076 + * @param integer $issue_id The issue ID
7077 + * @return integer The customer ID associated with the issue
7079 + function getContactID($issue_id)
7083 + $issue_id = Misc::escapeInteger($issue_id);
7085 + if (!empty($returns[$issue_id])) {
7086 + return $returns[$issue_id];
7090 + iss_customer_contact_id
7092 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7094 + iss_id=$issue_id";
7095 + $res = DB_Helper::getInstance()->getOne($stmt);
7096 + if (PEAR::isError($res)) {
7097 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7100 + $returns[$issue_id] = $res;
7107 + * Method used to get the project associated to a given issue.
7110 + * @param integer $issue_id The issue ID
7111 + * @param boolean $force_refresh If the cache should not be used.
7112 + * @return integer The project ID
7114 + function getProjectID($issue_id, $force_refresh = false)
7118 + $issue_id = Misc::escapeInteger($issue_id);
7120 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
7121 + return $returns[$issue_id];
7127 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7129 + iss_id=$issue_id";
7130 + $res = DB_Helper::getInstance()->getOne($stmt);
7131 + if (PEAR::isError($res)) {
7132 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7135 + $returns[$issue_id] = $res;
7142 + * Method used to remotely assign a given issue to an user.
7145 + * @param integer $issue_id The issue ID
7146 + * @param integer $usr_id The user ID of the person performing the change
7147 + * @param boolean $assignee The user ID of the assignee
7148 + * @return integer The status ID
7150 + function remoteAssign($issue_id, $usr_id, $assignee)
7152 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
7153 + // clear up the assignments for this issue, and then assign it to the current user
7154 + self::deleteUserAssociations($issue_id, $usr_id);
7155 + $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
7157 + // save a history entry about this...
7158 + History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
7159 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
7160 + if ($assignee != $usr_id) {
7161 + Notification::notifyNewAssignment(array($assignee), $issue_id);
7169 + * Method used to set the status of a given issue.
7172 + * @param integer $issue_id The issue ID
7173 + * @param integer $status_id The new status ID
7174 + * @param boolean $notify If a notification should be sent about this change.
7175 + * @return integer 1 if the update worked, -1 otherwise
7177 + function setStatus($issue_id, $status_id, $notify = false)
7179 + $issue_id = Misc::escapeInteger($issue_id);
7180 + $status_id = Misc::escapeInteger($status_id);
7182 + $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
7183 + if ($workflow !== true) {
7187 + // check if the status is already set to the 'new' one
7188 + if (self::getStatusID($issue_id) == $status_id) {
7192 + $old_status = self::getStatusID($issue_id);
7193 + $old_details = Status::getDetails($old_status);
7196 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7198 + iss_sta_id=$status_id,
7199 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7200 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7201 + iss_last_public_action_type='update'
7203 + iss_id=$issue_id";
7204 + $res = DB_Helper::getInstance()->query($stmt);
7205 + if (PEAR::isError($res)) {
7206 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7209 + // clear out the last-triggered-reminder flag when changing the status of an issue
7210 + Reminder_Action::clearLastTriggered($issue_id);
7212 + // if old status was closed and new status is not, clear closed data from issue.
7213 + if (@$old_details['sta_is_closed'] == 1) {
7214 + $new_details = Status::getDetails($status_id);
7215 + if ($new_details['sta_is_closed'] != 1) {
7216 + self::clearClosed($issue_id);
7221 + Notification::notifyStatusChange($issue_id, $old_status, $status_id);
7230 + * Method used to remotely set the status of a given issue.
7233 + * @param integer $issue_id The issue ID
7234 + * @param integer $usr_id The user ID of the person performing this change
7235 + * @param integer $new_status The new status ID
7236 + * @return integer 1 if the update worked, -1 otherwise
7238 + function setRemoteStatus($issue_id, $usr_id, $new_status)
7240 + $sta_id = Status::getStatusID($new_status);
7242 + $res = self::setStatus($issue_id, $sta_id);
7244 + // record history entry
7245 + History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
7252 + * Method used to set the release of an issue
7255 + * @param integer $issue_id The ID of the issue
7256 + * @param integer $pre_id The ID of the release to set this issue too
7257 + * @return integer 1 if the update worked, -1 otherwise
7259 + function setRelease($issue_id, $pre_id)
7261 + $issue_id = Misc::escapeInteger($issue_id);
7262 + $pre_id = Misc::escapeInteger($pre_id);
7264 + if ($pre_id != self::getRelease($issue_id)) {
7266 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7268 + iss_pre_id = $pre_id
7270 + iss_id = $issue_id";
7271 + $res = DB_Helper::getInstance()->query($sql);
7272 + if (PEAR::isError($res)) {
7273 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7283 + * Returns the current release of an issue
7286 + * @param integer $issue_id The ID of the issue
7287 + * @return integer The release
7289 + function getRelease($issue_id)
7294 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7296 + iss_id = " . Misc::escapeInteger($issue_id);
7297 + $res = DB_Helper::getInstance()->getOne($sql);
7298 + if (PEAR::isError($res)) {
7299 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7308 + * Method used to set the priority of an issue
7311 + * @param integer $issue_id The ID of the issue
7312 + * @param integer $pri_id The ID of the priority to set this issue too
7313 + * @return integer 1 if the update worked, -1 otherwise
7315 + function setPriority($issue_id, $pri_id)
7317 + $issue_id = Misc::escapeInteger($issue_id);
7318 + $pri_id = Misc::escapeInteger($pri_id);
7320 + if ($pri_id != self::getPriority($issue_id)) {
7322 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7324 + iss_pri_id = $pri_id
7326 + iss_id = $issue_id";
7327 + $res = DB_Helper::getInstance()->query($sql);
7328 + if (PEAR::isError($res)) {
7329 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7339 + * Returns the current issue priority
7342 + * @param integer $issue_id The ID of the issue
7343 + * @return integer The priority
7345 + function getPriority($issue_id)
7350 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7352 + iss_id = " . Misc::escapeInteger($issue_id);
7353 + $res = DB_Helper::getInstance()->getOne($sql);
7354 + if (PEAR::isError($res)) {
7355 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7364 + * Method used to set the category of an issue
7367 + * @param integer $issue_id The ID of the issue
7368 + * @param integer $prc_id The ID of the category to set this issue too
7369 + * @return integer 1 if the update worked, -1 otherwise
7371 + function setCategory($issue_id, $prc_id)
7373 + $issue_id = Misc::escapeInteger($issue_id);
7374 + $prc_id = Misc::escapeInteger($prc_id);
7376 + if ($prc_id != self::getPriority($issue_id)) {
7378 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7380 + iss_prc_id = $prc_id
7382 + iss_id = $issue_id";
7383 + $res = DB_Helper::getInstance()->query($sql);
7384 + if (PEAR::isError($res)) {
7385 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7395 + * Returns the current issue category
7398 + * @param integer $issue_id The ID of the issue
7399 + * @return integer The category
7401 + function getCategory($issue_id)
7406 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7408 + iss_id = " . Misc::escapeInteger($issue_id);
7409 + $res = DB_Helper::getInstance()->getOne($sql);
7410 + if (PEAR::isError($res)) {
7411 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7420 + * Method used to get all issues associated with a status that doesn't have
7421 + * the 'closed' context.
7424 + * @param integer $prj_id The project ID to list issues from
7425 + * @param integer $usr_id The user ID of the user requesting this information
7426 + * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
7427 + * @param integer $status_id The status ID to be used to restrict results
7428 + * @return array The list of open issues
7430 + function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
7432 + $prj_id = Misc::escapeInteger($prj_id);
7433 + $status_id = Misc::escapeInteger($status_id);
7434 + $projects = Project::getRemoteAssocListByUser($usr_id);
7435 + if (@count($projects) == 0) {
7445 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7446 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
7449 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
7453 + if (!empty($status_id)) {
7454 + $stmt .= " sta_id=$status_id AND ";
7457 + iss_prj_id=$prj_id AND
7458 + sta_id=iss_sta_id AND
7460 + if ($show_all_issues == false) {
7462 + isu_usr_id=$usr_id";
7464 + $stmt .= "\nGROUP BY
7466 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
7467 + if (PEAR::isError($res)) {
7468 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7471 + if (count($res) > 0) {
7472 + self::getAssignedUsersByIssues($res);
7480 + * Method used to build the required parameters to simulate an email reply
7481 + * to the user who reported the issue, using the issue details like summary
7482 + * and description as email fields.
7485 + * @param integer $issue_id The issue ID
7486 + * @return array The email parameters
7488 + function getReplyDetails($issue_id)
7490 + $issue_id = Misc::escapeInteger($issue_id);
7494 + usr_full_name AS reporter,
7495 + usr_email AS reporter_email,
7496 + iss_description AS description,
7497 + iss_summary AS sup_subject
7499 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7500 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
7502 + iss_usr_id=usr_id AND
7503 + iss_id=$issue_id";
7504 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
7505 + if (PEAR::isError($res)) {
7506 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7509 + $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
7510 + $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
7517 + * Method used to record the last updated timestamp for a given
7521 + * @param integer $issue_id The issue ID
7522 + * @param string $type The type of update that was made (optional)
7525 + function markAsUpdated($issue_id, $type = false)
7527 + $public = array("staff response", "customer action", "file uploaded", "user response");
7529 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7531 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
7532 + if ($type != false) {
7533 + if (in_array($type, $public)) {
7534 + $field = "iss_last_public_action_";
7536 + $field = "iss_last_internal_action_";
7538 + $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
7539 + $field . "type ='" . Misc::escapeString($type) . "'\n";
7542 + iss_id=" . Misc::escapeInteger($issue_id);
7543 + $res = DB_Helper::getInstance()->query($stmt);
7544 + if (PEAR::isError($res)) {
7545 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7548 + // update last response dates if this is a staff response
7549 + if ($type == "staff response") {
7551 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7553 + iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
7555 + iss_id = " . Misc::escapeInteger($issue_id);
7556 + DB_Helper::getInstance()->query($stmt);
7558 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7560 + iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
7562 + iss_first_response_date IS NULL AND
7563 + iss_id = " . Misc::escapeInteger($issue_id);
7564 + DB_Helper::getInstance()->query($stmt);
7573 + * Method used to check whether a given issue has duplicates
7577 + * @param integer $issue_id The issue ID
7580 + function hasDuplicates($issue_id)
7585 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7587 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
7588 + $res = DB_Helper::getInstance()->getOne($stmt);
7589 + if (PEAR::isError($res)) {
7590 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7603 + * Method used to update the duplicated issues for a given
7607 + * @param integer $issue_id The issue ID
7608 + * @return integer 1 if the update worked, -1 otherwise
7610 + function updateDuplicates($issue_id)
7612 + $issue_id = Misc::escapeInteger($issue_id);
7614 + $ids = self::getDuplicateList($issue_id);
7618 + $ids = @array_keys($ids);
7620 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7622 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7623 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7624 + iss_last_internal_action_type='updated',
7625 + iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
7626 + if (@$_POST["keep"] == "no") {
7627 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
7630 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
7631 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
7632 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
7634 + iss_id IN (" . implode(", ", $ids) . ")";
7635 + $res = DB_Helper::getInstance()->query($stmt);
7636 + if (PEAR::isError($res)) {
7637 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7640 + // record the change
7641 + for ($i = 0; $i < count($ids); $i++) {
7642 + History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
7643 + "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
7651 + * Method used to get a list of the duplicate issues for a given
7655 + * @param integer $issue_id The issue ID
7656 + * @return array The list of duplicates
7658 + function getDuplicateList($issue_id)
7660 + $res = self::getDuplicateDetailsList($issue_id);
7661 + if (@count($res) == 0) {
7665 + for ($i = 0; $i < count($res); $i++) {
7666 + $list[$res[$i]['issue_id']] = $res[$i]['title'];
7674 + * Method used to get a list of the duplicate issues (and their details)
7675 + * for a given issue ID.
7678 + * @param integer $issue_id The issue ID
7679 + * @return array The list of duplicates
7681 + function getDuplicateDetailsList($issue_id)
7685 + if (!empty($returns[$issue_id])) {
7686 + return $returns[$issue_id];
7691 + iss_summary title,
7692 + sta_title current_status,
7693 + sta_is_closed is_closed
7695 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7696 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
7698 + iss_sta_id=sta_id AND
7699 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
7700 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
7701 + if (PEAR::isError($res)) {
7702 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7705 + $returns[$issue_id] = $res;
7712 + * Method used to clear the duplicate status of an issue.
7715 + * @param integer $issue_id The issue ID
7716 + * @return integer 1 if the update worked, -1 otherwise
7718 + function clearDuplicateStatus($issue_id)
7720 + $issue_id = Misc::escapeInteger($issue_id);
7722 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7724 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7725 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7726 + iss_last_internal_action_type='updated',
7727 + iss_duplicated_iss_id=NULL
7729 + iss_id=$issue_id";
7730 + $res = DB_Helper::getInstance()->query($stmt);
7731 + if (PEAR::isError($res)) {
7732 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7735 + // record the change
7736 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
7743 + * Method used to mark an issue as a duplicate of an existing one.
7746 + * @param integer $issue_id The issue ID
7747 + * @return integer 1 if the update worked, -1 otherwise
7749 + function markAsDuplicate($issue_id)
7751 + $issue_id = Misc::escapeInteger($issue_id);
7752 + if (!self::exists($issue_id)) {
7757 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7759 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7760 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7761 + iss_last_internal_action_type='updated',
7762 + iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
7764 + iss_id=$issue_id";
7765 + $res = DB_Helper::getInstance()->query($stmt);
7766 + if (PEAR::isError($res)) {
7767 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7770 + if (!empty($_POST["comments"])) {
7771 + // add note with the comments of marking an issue as a duplicate of another one
7772 + $_POST['title'] = 'Issue duplication comments';
7773 + $_POST["note"] = $_POST["comments"];
7774 + Note::insert(Auth::getUserID(), $issue_id);
7776 + // record the change
7777 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
7778 + "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
7784 + function isDuplicate($issue_id)
7789 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7791 + iss_id = " . Misc::escapeInteger($issue_id) . " AND
7792 + iss_duplicated_iss_id IS NULL";
7793 + $res = DB_Helper::getInstance()->getOne($sql);
7794 + if (PEAR::isError($res)) {
7795 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7807 + * Method used to get an associative array of user ID => user
7808 + * status associated with a given issue ID.
7811 + * @param integer $issue_id The issue ID
7812 + * @return array The list of users
7814 + function getAssignedUsersStatus($issue_id)
7820 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
7821 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
7823 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
7824 + isu_usr_id=usr_id";
7825 + $res = DB_Helper::getInstance()->getAssoc($stmt);
7826 + if (PEAR::isError($res)) {
7827 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7836 + * Method used to get the summary associated with a given issue ID.
7839 + * @param integer $issue_id The issue ID
7840 + * @return string The issue summary
7842 + function getTitle($issue_id)
7847 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7849 + iss_id=" . Misc::escapeInteger($issue_id);
7850 + $res = DB_Helper::getInstance()->getOne($stmt);
7851 + if (PEAR::isError($res)) {
7852 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7861 + * Method used to get the issue ID associated with a specific summary.
7864 + * @param string $summary The summary to look for
7865 + * @return integer The issue ID
7867 + function getIssueID($summary)
7872 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7874 + iss_summary='" . Misc::escapeString($summary) . "'";
7875 + $res = DB_Helper::getInstance()->getOne($stmt);
7876 + if (PEAR::isError($res)) {
7877 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7880 + if (empty($res)) {
7890 + * Method used to add a new anonymous based issue in the system.
7893 + * @return integer The new issue ID
7895 + function addAnonymousReport()
7897 + $options = Project::getAnonymousPostOptions($_POST["project"]);
7898 + $initial_status = Project::getInitialStatus($_POST["project"]);
7899 + $stmt = "INSERT INTO
7900 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7907 + if (!empty($initial_status)) {
7908 + $stmt .= "iss_sta_id,";
7912 + iss_last_public_action_date,
7913 + iss_last_public_action_type,
7916 + iss_root_message_id
7918 + " . Misc::escapeInteger($_POST["project"]) . ",
7919 + " . $options["category"] . ",
7921 + " . $options["priority"] . ",
7922 + " . $options["reporter"] . ",";
7923 + if (!empty($initial_status)) {
7924 + $stmt .= "$initial_status,";
7927 + '" . Date_Helper::getCurrentDateGMT() . "',
7928 + '" . Date_Helper::getCurrentDateGMT() . "',
7930 + '" . Misc::escapeString($_POST["summary"]) . "',
7931 + '" . Misc::escapeString($_POST["description"]) . "',
7932 + '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
7934 + $res = DB_Helper::getInstance()->query($stmt);
7935 + if (PEAR::isError($res)) {
7936 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7939 + $new_issue_id = DB_Helper::get_last_insert_id();
7940 + // log the creation of the issue
7941 + History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
7943 + // now process any files being uploaded
7945 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
7946 + if (!@empty($_FILES["file"]["name"][$i])) {
7952 + $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
7953 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
7954 + $filename = @$_FILES["file"]["name"][$i];
7955 + if (empty($filename)) {
7958 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
7959 + if (!empty($blob)) {
7960 + Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
7964 + // need to process any custom fields ?
7965 + if (@count($_POST["custom_fields"]) > 0) {
7966 + foreach ($_POST["custom_fields"] as $fld_id => $value) {
7967 + Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
7971 + // now add the user/issue association
7972 + $assign = array();
7973 + $users = @$options["users"];
7974 + $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
7975 + for ($i = 0; $i < count($users); $i++) {
7976 + Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
7977 + self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
7978 + $assign[] = $users[$i];
7981 + Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
7983 + // also notify any users that want to receive emails anytime a new issue is created
7984 + Notification::notifyNewIssue($_POST['project'], $new_issue_id);
7986 + return $new_issue_id;
7992 + * Method used to remove all issues associated with a specific list of
7996 + * @param array $ids The list of projects to look for
7999 + function removeByProjects($ids)
8001 + $items = @implode(", ", Misc::escapeInteger($ids));
8005 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8007 + iss_prj_id IN ($items)";
8008 + $res = DB_Helper::getInstance()->getCol($stmt);
8009 + if (PEAR::isError($res)) {
8010 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8013 + if (count($res) > 0) {
8014 + self::deleteAssociations($res);
8015 + Attachment::removeByIssues($res);
8016 + SCM::removeByIssues($res);
8017 + Impact_Analysis::removeByIssues($res);
8018 + self::deleteUserAssociations($res);
8019 + Note::removeByIssues($res);
8020 + Time_Tracking::removeByIssues($res);
8021 + Notification::removeByIssues($res);
8022 + Custom_Field::removeByIssues($res);
8023 + Phone_Support::removeByIssues($res);
8024 + History::removeByIssues($res);
8025 + // now really delete the issues
8026 + $items = implode(", ", $res);
8027 + $stmt = "DELETE FROM
8028 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8030 + iss_id IN ($items)";
8031 + DB_Helper::getInstance()->query($stmt);
8039 + * Method used to close off an issue.
8042 + * @param integer $usr_id The user ID
8043 + * @param integer $issue_id The issue ID
8044 + * @param bool $send_notification Whether to send a notification about this action or not
8045 + * @param integer $resolution_id The resolution ID
8046 + * @param integer $status_id The status ID
8047 + * @param string $reason The reason for closing this issue
8048 + * @param string $send_notification_to Who this notification should be sent too
8049 + * @return integer 1 if the update worked, -1 otherwise
8051 + function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
8053 + $usr_id = Misc::escapeInteger($usr_id);
8054 + $issue_id = Misc::escapeInteger($issue_id);
8055 + $resolution_id = Misc::escapeInteger($resolution_id);
8056 + $status_id = Misc::escapeInteger($status_id);
8059 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8061 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8062 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8063 + iss_last_public_action_type='closed',
8064 + iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
8065 + if (!empty($resolution_id)) {
8066 + $stmt .= "iss_res_id=$resolution_id,\n";
8068 + $stmt .= "iss_sta_id=$status_id
8070 + iss_id=$issue_id";
8071 + $res = DB_Helper::getInstance()->query($stmt);
8072 + if (PEAR::isError($res)) {
8073 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8076 + self::moveOrderForAllUsers($issue_id, 1000);
8077 + $prj_id = self::getProjectID($issue_id);
8079 + // record the change
8080 + History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
8082 + if ($send_notification_to == 'all') {
8084 + $from = User::getFromHeader($usr_id);
8085 + $message_id = User::getFromHeader($usr_id);
8086 + $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
8087 + '', '', 'Issue closed comments', $reason, '');
8089 + $structure = Mime_Helper::decode($full_email, true, false);
8092 + 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
8093 + 'issue_id' => $issue_id,
8094 + 'message_id' => $message_id,
8095 + 'date' => Date_Helper::getCurrentDateGMT(),
8096 + 'subject' => 'Issue closed comments',
8098 + 'has_attachment'=> 0,
8099 + 'body' => $reason,
8100 + 'full_email' => $full_email,
8101 + 'headers' => $structure->headers
8103 + Support::insertEmail($email, $structure, $sup_id, true);
8106 + // add note with the reason to close the issue
8107 + $_POST['title'] = 'Issue closed comments';
8108 + $_POST["note"] = $reason;
8109 + Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
8113 + if ($send_notification) {
8114 + if (Customer::hasCustomerIntegration($prj_id)) {
8115 + // send a special confirmation email when customer issues are closed
8117 + iss_customer_contact_id
8119 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8121 + iss_id=$issue_id";
8122 + $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
8123 + if (!empty($customer_contact_id)) {
8124 + Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
8127 + // send notifications for the issue being closed
8128 + Notification::notify($issue_id, 'closed', $ids);
8130 + Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
8137 + * Method used to update the details of a specific issue.
8140 + * @param integer $issue_id The issue ID
8141 + * @return integer 1 if the update worked, -1 or -2 otherwise
8143 + function update($issue_id)
8146 + $errors = array();
8148 + $issue_id = Misc::escapeInteger($issue_id);
8150 + $usr_id = Auth::getUserID();
8151 + $prj_id = self::getProjectID($issue_id);
8153 + $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
8154 + if ($workflow !== true) {
8158 + // get all of the 'current' information of this issue
8159 + $current = self::getDetails($issue_id);
8160 + // update the issue associations
8161 + if (empty($_POST['associated_issues'])) {
8162 + $associated_issues = array();
8164 + $associated_issues = explode(',', @$_POST['associated_issues']);
8165 + // make sure all associated issues are valid (and in this project)
8166 + for ($i = 0; $i < count($associated_issues); $i++) {
8167 + if (!self::exists(trim($associated_issues[$i]), false)) {
8168 + $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
8169 + unset($associated_issues[$i]);
8173 + $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
8174 + if (count($association_diff) > 0) {
8175 + // go through the new assocations, if association already exists, skip it
8176 + $associations_to_remove = $current['associated_issues'];
8177 + if (count($associated_issues) > 0) {
8178 + foreach ($associated_issues as $index => $associated_id) {
8179 + if (!in_array($associated_id, $current['associated_issues'])) {
8180 + self::addAssociation($issue_id, $associated_id, $usr_id);
8182 + // already assigned, remove this user from list of users to remove
8183 + unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
8187 + if (count($associations_to_remove) > 0) {
8188 + foreach ($associations_to_remove as $associated_id) {
8189 + self::deleteAssociation($issue_id, $associated_id);
8193 + $assignments_changed = false;
8194 + if (@$_POST["keep_assignments"] == "no") {
8195 + // only change the issue-user associations if there really were any changes
8196 + $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
8197 + if (!empty($_POST['assignments'])) {
8198 + $new_assignees = @$_POST['assignments'];
8200 + $new_assignees = array();
8202 + $assignment_notifications = array();
8204 + // remove people from the assignment list, if appropriate
8205 + foreach ($old_assignees as $assignee) {
8206 + if (!in_array($assignee, $new_assignees)) {
8207 + self::deleteUserAssociation($issue_id, $assignee);
8208 + $assignments_changed = true;
8211 + // add people to the assignment list, if appropriate
8212 + foreach ($new_assignees as $assignee) {
8213 + if (!in_array($assignee, $old_assignees)) {
8214 + self::addUserAssociation($usr_id, $issue_id, $assignee);
8215 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
8216 + $assignment_notifications[] = $assignee;
8217 + $assignments_changed = true;
8220 + if (count($assignment_notifications) > 0) {
8221 + Notification::notifyNewAssignment($assignment_notifications, $issue_id);
8224 + if (empty($_POST["estimated_dev_time"])) {
8225 + $_POST["estimated_dev_time"] = 0;
8228 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8230 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8231 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8232 + iss_last_public_action_type='updated',";
8233 + if (!empty($_POST["category"])) {
8234 + $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
8236 + if (@$_POST["keep"] == "no") {
8237 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
8239 + if (!empty($_POST['expected_resolution_date'])) {
8240 + $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
8242 + $stmt .= "iss_expected_resolution_date=null,";
8245 + iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
8246 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
8247 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
8248 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
8249 + iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
8250 + iss_description='" . Misc::escapeString($_POST["description"]) . "',
8251 + iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
8252 + iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
8253 + iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
8254 + iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
8255 + if (isset($_POST['private'])) {
8257 + iss_private = " . Misc::escapeInteger($_POST['private']);
8261 + iss_id=$issue_id";
8262 + $res = DB_Helper::getInstance()->query($stmt);
8263 + if (PEAR::isError($res)) {
8264 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8267 + // add change to the history (only for changes on specific fields?)
8268 + $updated_fields = array();
8269 + if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
8270 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
8272 + if ($current["iss_prc_id"] != $_POST["category"]) {
8273 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
8275 + if ($current["iss_pre_id"] != $_POST["release"]) {
8276 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
8278 + if ($current["iss_pri_id"] != $_POST["priority"]) {
8279 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
8280 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
8282 + if ($current["iss_sta_id"] != $_POST["status"]) {
8283 + // clear out the last-triggered-reminder flag when changing the status of an issue
8284 + Reminder_Action::clearLastTriggered($issue_id);
8286 + // if old status was closed and new status is not, clear closed data from issue.
8287 + $old_status_details = Status::getDetails($current['iss_sta_id']);
8288 + if ($old_status_details['sta_is_closed'] == 1) {
8289 + $new_status_details = Status::getDetails($_POST["status"]);
8290 + if ($new_status_details['sta_is_closed'] != 1) {
8291 + self::clearClosed($issue_id);
8294 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
8296 + if ($current["iss_res_id"] != $_POST["resolution"]) {
8297 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
8299 + if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
8300 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
8302 + if ($current["iss_summary"] != $_POST["summary"]) {
8303 + $updated_fields["Summary"] = '';
8305 + if ($current["iss_description"] != $_POST["description"]) {
8306 + $updated_fields["Description"] = '';
8308 + if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
8309 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
8311 + if (count($updated_fields) > 0) {
8312 + // log the changes
8315 + foreach ($updated_fields as $key => $value) {
8319 + if (($key != "Summary") && ($key != "Description")) {
8320 + $changes .= "$key: $value";
8322 + $changes .= "$key";
8326 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
8327 + // send notifications for the issue being updated
8328 + Notification::notifyIssueUpdated($issue_id, $current, $_POST);
8331 + // record group change as a seperate change
8332 + if ($current["iss_grp_id"] != (int)$_POST["group"]) {
8333 + History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
8334 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
8337 + // now update any duplicates, if any
8338 + $update_dupe = array(
8345 + // COMPAT: the following line requires PHP > 4.0.4
8346 + $intersect = array_intersect($update_dupe, array_keys($updated_fields));
8347 + if (($current["duplicates"] != '') && (count($intersect) > 0)) {
8348 + self::updateDuplicates($issue_id);
8351 + // if there is customer integration, mark last customer action
8352 + if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
8353 + self::recordLastCustomerAction($issue_id);
8356 + if ($assignments_changed) {
8357 + // XXX: we may want to also send the email notification for those "new" assignees
8358 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
8361 + Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
8362 + // Move issue to another project
8363 + if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
8364 + $new_prj_id = (int)@$_POST['new_prj'];
8365 + if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
8366 + if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
8367 + $res = self::moveIssue($issue_id, $new_prj_id);
8381 + * Method used to update the a single detail field of a specific issue.
8383 + * @param integer $issue_id
8384 + * @param string $field_name
8385 + * @param string $field_value
8386 + * @param string $field_type string or integer (for escape)
8387 + * @return integer 1 on success, -1 otherwise
8389 + function updateField($issue_id, $field_name, $filed_value) {
8391 + $issue_id = Misc::escapeInteger($issue_id);
8393 + $usr_id = Auth::getUserID();
8394 + $prj_id = self::getProjectID($issue_id);
8396 + // get all of the 'current' information of this issue
8397 + $current = self::getDetails($issue_id);
8400 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8402 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8403 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8404 + iss_last_public_action_type='updated'";
8406 + switch ($field_name) {
8408 + $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
8411 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
8413 + case 'expected_resolution_date':
8414 + $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
8417 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
8420 + $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
8423 + $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
8425 + case 'resolution':
8426 + $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
8429 + $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
8431 + case 'description':
8432 + $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
8434 + case 'estimated_dev_time':
8435 + $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
8437 + case 'percent_complete':
8438 + $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
8440 + case 'trigger_reminders':
8441 + $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
8444 + $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
8447 + $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
8450 + Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
8457 + iss_id=$issue_id";
8459 + $res = DB_Helper::getInstance()->query($stmt);
8460 + if (PEAR::isError($res)) {
8461 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8465 + 'category' => $current['iss_prc_id'],
8466 + 'release' => $current['iss_pre_id'],
8467 + 'expected_resolution_date' => $current['iss_expected_resolution_date'],
8468 + 'release' => $current['iss_pre_id'],
8469 + 'priority' => $current['iss_pri_id'],
8470 + 'status' => $current['iss_sta_id'],
8471 + 'resolution' => $current['iss_res_id'],
8472 + 'summary' => $current['iss_summary'],
8473 + 'description' => $current['iss_description'],
8474 + 'estimated_dev_time' => $current['iss_dev_time'],
8475 + 'percent_complete' => $current['iss_percent_complete'],
8476 + 'trigger_reminders' => $current['iss_trigger_reminders'],
8477 + 'group' => $current['iss_grp_id'],
8478 + 'iss_private' => $current['private']
8480 + $new[$field_name] = $filed_value;
8482 + // add change to the history (only for changes on specific fields?)
8483 + $updated_fields = array();
8484 + if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
8485 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
8487 + if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
8488 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
8490 + if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
8491 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
8493 + if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
8494 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
8495 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
8497 + if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
8498 + // clear out the last-triggered-reminder flag when changing the status of an issue
8499 + Reminder_Action::clearLastTriggered($issue_id);
8501 + // if old status was closed and new status is not, clear closed data from issue.
8502 + $old_status_details = Status::getDetails($current['iss_sta_id']);
8503 + if ($old_status_details['sta_is_closed'] == 1) {
8504 + $new_status_details = Status::getDetails($filed_value);
8505 + if ($new_status_details['sta_is_closed'] != 1) {
8506 + self::clearClosed($issue_id);
8509 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
8511 + if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
8512 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
8514 + if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
8515 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
8517 + if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
8518 + $updated_fields["Summary"] = '';
8520 + if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
8521 + $updated_fields["Description"] = '';
8523 + if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
8524 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
8526 + if (count($updated_fields) > 0) {
8527 + // log the changes
8530 + foreach ($updated_fields as $key => $value) {
8534 + if (($key != "Summary") && ($key != "Description")) {
8535 + $changes .= "$key: $value";
8537 + $changes .= "$key";
8542 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
8543 + // send notifications for the issue being updated
8544 + Notification::notifyIssueUpdated($issue_id, $current, $new);
8552 + * Move the issue to a new project
8554 + * @param integer $issue_id
8555 + * @param integer $new_prj_id
8556 + * @return integer 1 on success, -1 otherwise
8558 + function moveIssue($issue_id, $new_prj_id)
8561 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8563 + iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
8565 + iss_id = " . Misc::escapeInteger($issue_id);
8566 + $res = DB_Helper::getInstance()->query($stmt);
8567 + if (PEAR::isError($res)) {
8568 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8571 + $currentDetails = self::getDetails($issue_id);
8573 + // set new category
8574 + $new_iss_prc_list = Category::getAssocList($new_prj_id);
8575 + $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
8576 + $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
8577 + if ($new_prc_id === false) {
8578 + // use the first category listed in the new project
8579 + $new_prc_id = key($new_iss_prc_list);
8582 + // set new priority
8583 + $new_iss_pri_list = Priority::getAssocList($new_prj_id);
8584 + $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
8585 + $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
8586 + if ($new_pri_id === false) {
8587 + // use the first category listed in the new project
8588 + $new_pri_id = key($new_iss_pri_list);
8591 + // XXX: Set status if needed when moving issue
8594 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8596 + iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
8597 + iss_pri_id=" . $new_pri_id . "
8599 + iss_id=$issue_id";
8600 + $res = DB_Helper::getInstance()->query($stmt);
8601 + if (PEAR::isError($res)) {
8602 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8605 + // clear project cache
8606 + self::getProjectID($issue_id, true);
8608 + Notification::notifyNewIssue($new_prj_id, $issue_id);
8614 + * Method used to associate an existing issue with another one.
8617 + * @param integer $issue_id The issue ID
8618 + * @param integer $issue_id The other issue ID
8621 + function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
8623 + $issue_id = Misc::escapeInteger($issue_id);
8624 + $associated_id = Misc::escapeInteger($associated_id);
8626 + $stmt = "INSERT INTO
8627 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8635 + DB_Helper::getInstance()->query($stmt);
8636 + History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
8637 + // link the associated issue back to this one
8638 + if ($link_issues) {
8639 + self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
8645 + * Method used to remove the issue associations related to a specific issue.
8648 + * @param integer $issue_id The issue ID
8651 + function deleteAssociations($issue_id, $usr_id = FALSE)
8653 + $issue_id = Misc::escapeInteger($issue_id);
8654 + if (is_array($issue_id)) {
8655 + $issue_id = implode(", ", $issue_id);
8657 + $stmt = "DELETE FROM
8658 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8660 + isa_issue_id IN ($issue_id) OR
8661 + isa_associated_id IN ($issue_id)";
8662 + DB_Helper::getInstance()->query($stmt);
8664 + History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
8670 + * Method used to remove a issue association from an issue.
8673 + * @param integer $issue_id The issue ID
8674 + * @param integer $associated_id The associated issue ID to remove.
8677 + function deleteAssociation($issue_id, $associated_id)
8679 + $issue_id = Misc::escapeInteger($issue_id);
8680 + $associated_id = Misc::escapeInteger($associated_id);
8681 + $stmt = "DELETE FROM
8682 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8685 + isa_issue_id = $issue_id AND
8686 + isa_associated_id = $associated_id
8689 + isa_issue_id = $associated_id AND
8690 + isa_associated_id = $issue_id
8692 + DB_Helper::getInstance()->query($stmt);
8693 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
8694 + "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
8695 + History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
8696 + "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
8701 + * Method used to assign an issue with an user.
8704 + * @param integer $usr_id The user ID of the person performing this change
8705 + * @param integer $issue_id The issue ID
8706 + * @param integer $assignee_usr_id The user ID of the assignee
8707 + * @param boolean $add_history Whether to add a history entry about this or not
8708 + * @return integer 1 if the update worked, -1 otherwise
8710 + function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
8712 + $issue_id = Misc::escapeInteger($issue_id);
8713 + $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
8715 + // move all orders down to free "order space" for this new association
8717 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8719 + isu_order = isu_order + 1
8721 + isu_usr_id = $assignee_usr_id AND
8722 + isu_order >= $order";
8723 + $res = DB_Helper::getInstance()->query($stmt);
8724 + if (PEAR::isError($res)) {
8725 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8728 + // insert the new association
8729 + $stmt = "INSERT INTO
8730 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8734 + isu_assigned_date,
8739 + '" . Date_Helper::getCurrentDateGMT() . "',
8742 + $res = DB_Helper::getInstance()->query($stmt);
8743 + if (PEAR::isError($res)) {
8744 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8747 + if ($add_history) {
8748 + History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
8749 + 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
8756 + * Method used to get the order list to be rearranged
8759 + * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
8760 + * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
8761 + * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
8763 + function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
8765 + // find all affected associantion orders
8766 + $stmt = "SELECT isu_usr_id, isu_order FROM
8767 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8769 + isu_iss_id IN ($issue_id)";
8770 + if ($usr_id !== FALSE) {
8771 + $stmt.= " AND isu_usr_id IN ($usr_id)";
8773 + $stmt.= "ORDER BY isu_order";
8774 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
8775 + if (PEAR::isError($res)) {
8776 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8779 + $deleted_orders = array();
8780 + foreach ($res as $row) {
8781 + if (empty($deleted_orders[$row['isu_usr_id']])) {
8782 + $deleted_orders[$row['isu_usr_id']] = array();
8784 + $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
8786 + return $deleted_orders;
8792 + * Method used to rearrange order list in the db according to known deleted records
8795 + * @param mixed deleteorder list
8798 + function rearrangeDeleteUserAssociationOrderList($delete_order_list)
8800 + if (empty($delete_order_list) || (!is_array($delete_order_list))) {
8803 + foreach ($delete_order_list as $isu_usr_id => $orders) {
8804 + for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
8805 + // move the orders after them up to take the "order space" of the deleted records
8807 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8809 + isu_order = isu_order - " . ($i+1) . "
8811 + isu_usr_id = $isu_usr_id AND
8812 + isu_order > " . $orders[$i];
8813 + if ($i < count($orders) - 1) {
8815 + isu_order < " . $orders[$i+1];
8817 + $res = DB_Helper::getInstance()->query($stmt);
8818 + if (PEAR::isError($res)) {
8819 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8829 + * Method used to delete all user assignments for a specific issue.
8832 + * @param integer $issue_id The issue ID
8833 + * @param integer $usr_id The user ID of the person performing the change
8836 + function deleteUserAssociations($issue_id, $usr_id = FALSE)
8838 + $issue_id = Misc::escapeInteger($issue_id);
8839 + if (is_array($issue_id)) {
8840 + $issue_id = implode(", ", $issue_id);
8842 + $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
8843 + $stmt = "DELETE FROM
8844 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8846 + isu_iss_id IN ($issue_id)";
8847 + $res = DB_Helper::getInstance()->query($stmt);
8848 + if (PEAR::isError($res)) {
8849 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8853 + History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
8855 + self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
8862 + * Method used to delete a single user assignments for a specific issue.
8865 + * @param integer $issue_id The issue ID
8866 + * @param integer $usr_id The user to remove.
8867 + * @param boolean $add_history Whether to add a history entry about this or not
8870 + function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
8872 + $issue_id = Misc::escapeInteger($issue_id);
8873 + $usr_id = Misc::escapeInteger($usr_id);
8874 + $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
8875 + $stmt = "DELETE FROM
8876 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8878 + isu_iss_id = $issue_id AND
8879 + isu_usr_id = $usr_id";
8880 + $res = DB_Helper::getInstance()->query($stmt);
8881 + if (PEAR::isError($res)) {
8882 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8885 + if ($add_history) {
8886 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
8887 + User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
8889 + self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
8896 + * Creates an issue with the given email information.
8899 + * @param integer $prj_id The project ID
8900 + * @param integer $usr_id The user responsible for this action
8901 + * @param string $sender The original sender of this email
8902 + * @param string $summary The issue summary
8903 + * @param string $description The issue description
8904 + * @param integer $category The category ID
8905 + * @param integer $priority The priority ID
8906 + * @param array $assignment The list of users to assign this issue to
8907 + * @param string $date The date the email was originally sent.
8908 + * @param string $msg_id The message ID of the email we are creating this issue from.
8911 + function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
8914 + $exclude_list = array();
8916 + $sender_email = Mail_Helper::getEmailAddress($sender);
8917 + $sender_usr_id = User::getUserIDByEmail($sender_email, true);
8918 + if (!empty($sender_usr_id)) {
8919 + $reporter = $sender_usr_id;
8920 + $exclude_list[] = $sender_usr_id;
8924 + 'category' => $category,
8925 + 'priority' => $priority,
8926 + 'description' => $description,
8927 + 'summary' => $summary,
8928 + 'msg_id' => $msg_id,
8931 + if (Customer::hasCustomerIntegration($prj_id)) {
8932 + list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
8933 + if (!empty($customer_id)) {
8934 + $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
8935 + // overwrite the reporter with the customer contact
8936 + $reporter = User::getUserIDByContactID($customer_contact_id);
8937 + $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
8939 + $data['customer'] = $customer_id;
8940 + $data['contact'] = $customer_contact_id;
8941 +# $data['contract'] = // XXX missing
8942 + $data['contact_person_lname'] = $contact['last_name'];
8943 + $data['contact_person_fname'] = $contact['first_name'];
8944 + $data['contact_email'] = $sender_email;
8945 + $data['contact_phone'] = $contact['phone'];
8946 + $data['contact_timezone'] = $contact_timezone;
8949 + $customer_id = FALSE;
8951 + if (empty($reporter)) {
8952 + $reporter = APP_SYSTEM_USER_ID;
8955 + $data['reporter'] = $reporter;
8957 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
8958 + if ($issue_id == -1) {
8964 + // log the creation of the issue
8965 + History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
8967 + $emails = array();
8968 + $manager_usr_ids = array();
8969 + if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
8970 + // if there are any technical account managers associated with this customer, add these users to the notification list
8971 + $managers = Customer::getAccountManagers($prj_id, $customer_id);
8972 + $manager_usr_ids = array_keys($managers);
8973 + $manager_emails = array_values($managers);
8974 + $emails = array_merge($emails, $manager_emails);
8976 + // add the reporter to the notification list
8977 + $emails[] = $sender;
8978 + $emails = array_unique($emails);
8979 + $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
8980 + foreach ($emails as $address) {
8981 + Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
8984 + // only assign the issue to an user if the associated customer has any technical account managers
8986 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
8987 + foreach ($manager_usr_ids as $manager_usr_id) {
8988 + $users[] = $manager_usr_id;
8989 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
8990 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
8994 + // now add the user/issue association
8995 + if (@count($assignment) > 0) {
8996 + for ($i = 0; $i < count($assignment); $i++) {
8997 + Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
8998 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
8999 + if ($assignment[$i] != $usr_id) {
9000 + $users[] = $assignment[$i];
9004 + // only use the round-robin feature if this new issue was not
9005 + // already assigned to a customer account manager
9006 + if (@count($manager_usr_ids) < 1) {
9007 + $assignee = Round_Robin::getNextAssignee($prj_id);
9008 + // assign the issue to the round robin person
9009 + if (!empty($assignee)) {
9010 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
9011 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
9012 + $users[] = $assignee;
9017 + if (count($users) > 0) {
9018 + $has_assignee = true;
9021 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
9023 + // send special 'an issue was auto-created for you' notification back to the sender
9024 + Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
9026 + // also notify any users that want to receive emails anytime a new issue is created
9027 + Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
9034 + * Return errors that happened when creating new issue from POST method.
9038 + private static $insert_errors = array();
9039 + static function getInsertErrors() {
9040 + return self::$insert_errors;
9044 + * Method used to add a new issue using the normal report form.
9047 + * @return integer The new issue ID
9049 + function createFromPost()
9052 + 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
9053 + 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
9054 + 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
9057 + foreach ($keys as $key) {
9058 + if (isset($_POST[$key])) {
9059 + $data[$key] = $_POST[$key];
9063 + $prj_id = Auth::getCurrentProject();
9064 + $usr_id = Auth::getUserID();
9066 + // if we are creating an issue for a customer, put the
9067 + // main customer contact as the reporter for it
9068 + if (Customer::hasCustomerIntegration($prj_id)) {
9069 + $contact_usr_id = User::getUserIDByContactID($data['contact']);
9070 + if (empty($contact_usr_id)) {
9071 + $contact_usr_id = $usr_id;
9073 + $data['reporter'] = $contact_usr_id;
9075 + $data['reporter'] = $usr_id;
9078 + $data['msg_id'] = Mail_Helper::generateMessageID();
9080 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
9081 + if ($issue_id == -1) {
9087 + $info = User::getNameEmail($usr_id);
9088 + // log the creation of the issue
9089 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
9091 + $emails = array();
9092 + if (Customer::hasCustomerIntegration($prj_id)) {
9093 + if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
9094 + $emails = $data['contact_extra_emails'];
9096 + // add the primary contact to the notification list
9097 + if ($data['add_primary_contact'] == 'yes') {
9098 + $contact_email = User::getEmailByContactID($data['contact']);
9099 + if (!empty($contact_email)) {
9100 + $emails[] = $contact_email;
9103 + // if there are any technical account managers associated with this customer, add these users to the notification list
9104 + $managers = Customer::getAccountManagers($prj_id, $data['customer']);
9105 + $manager_usr_ids = array_keys($managers);
9106 + $manager_emails = array_values($managers);
9107 + $emails = array_merge($emails, $manager_emails);
9109 + // add the reporter to the notification list
9110 + $emails[] = $info['usr_email'];
9111 + $emails = array_unique($emails);
9112 + foreach ($emails as $address) {
9113 + Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
9116 + // only assign the issue to an user if the associated customer has any technical account managers
9119 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
9120 + foreach ($manager_usr_ids as $manager_usr_id) {
9121 + $users[] = $manager_usr_id;
9122 + self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
9123 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
9127 + // now add the user/issue association (aka assignments)
9128 + if (!empty($data['users']) && count($data['users']) > 0) {
9129 + for ($i = 0; $i < count($data['users']); $i++) {
9130 + Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
9131 + Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
9132 + self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
9133 + if ($data['users'][$i] != $usr_id) {
9134 + $users[] = $data['users'][$i];
9138 + // only use the round-robin feature if this new issue was not
9139 + // already assigned to a customer account manager
9140 + if (@count($manager_usr_ids) < 1) {
9141 + $assignee = Round_Robin::getNextAssignee($prj_id);
9142 + // assign the issue to the round robin person
9143 + if (!empty($assignee)) {
9144 + $users[] = $assignee;
9145 + self::addUserAssociation($usr_id, $issue_id, $assignee, false);
9146 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
9152 + // now process any files being uploaded
9154 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
9155 + if (!@empty($_FILES["file"]["name"][$i])) {
9162 + for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
9163 + $filename = @$_FILES["file"]["name"][$i];
9164 + if (empty($filename)) {
9167 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
9168 + if (empty($blob)) {
9169 + // error reading a file
9170 + self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
9174 + "filename" => $filename,
9175 + "type" => $_FILES['file']['type'][$i],
9179 + if (count($files) > 0) {
9180 + $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
9181 + foreach ($files as $file) {
9182 + Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
9186 + // need to associate any emails ?
9187 + if (!empty($data['attached_emails'])) {
9188 + $items = explode(",", $data['attached_emails']);
9189 + Support::associate($usr_id, $issue_id, $items);
9191 + // need to notify any emails being converted into issues ?
9192 + if (@count($data['notify_senders']) > 0) {
9193 + $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
9195 + $recipients = array();
9197 + // need to process any custom fields ?
9198 + if (@count($data['custom_fields']) > 0) {
9199 + foreach ($data['custom_fields'] as $fld_id => $value) {
9200 + Custom_Field::associateIssue($issue_id, $fld_id, $value);
9203 + // also send a special confirmation email to the customer contact
9204 + if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
9205 + // also need to pass the list of sender emails already notified,
9206 + // so we can avoid notifying the same person again
9207 + $contact_email = User::getEmailByContactID($data['contact']);
9208 + if (@!in_array($contact_email, $recipients)) {
9209 + Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
9213 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
9215 + // also notify any users that want to receive emails anytime a new issue is created
9216 + Notification::notifyNewIssue($prj_id, $issue_id);
9222 + * Insert issue to database.
9224 + * @param integer $prj_id The project ID
9225 + * @param integer $usr_id The user responsible for this action
9226 + * @param array $data of issue to be inserted
9227 + * @return integer The new issue ID
9229 + private function insertIssue($prj_id, $usr_id, $data)
9232 + // XXX missing_fields never used
9233 + $missing_fields = array();
9234 + if ($data['category'] == -1) {
9235 + $missing_fields[] = 'Category';
9237 + if ($data['priority'] == -1) {
9238 + $missing_fields[] = 'Priority';
9241 + // if there is no reporter set, use the system user
9242 + if (empty($data['reporter'])) {
9243 + $data['reporter'] = APP_SYSTEM_USER_ID;
9246 + if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
9247 + $data['estimated_dev_time'] = 0;
9251 + $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
9253 + "iss_prj_id=" . $prj_id . ",";
9254 + if (!empty($data['group'])) {
9255 + $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
9257 + if (!empty($data['category'])) {
9258 + $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
9260 + if (!empty($data['release'])) {
9261 + $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
9263 + if (!empty($data['priority'])) {
9264 + $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
9267 + $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
9269 + $initial_status = Project::getInitialStatus($prj_id);
9270 + if (!empty($initial_status)) {
9271 + $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
9274 + if (Customer::hasCustomerIntegration($prj_id)) {
9276 + iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
9277 + if (!empty($data['contact'])) {
9279 + iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
9282 + iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
9283 + iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
9284 + iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
9285 + iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
9286 + iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
9287 + iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
9291 + iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
9292 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
9293 + iss_last_public_action_type='created',
9294 + iss_summary='" . Misc::escapeString($data['summary']) . "',
9295 + iss_description='" . Misc::escapeString($data['description']) . "',
9296 + iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
9297 + if (!empty($data['contact'])) {
9299 + iss_private=" . Misc::escapeInteger($data['private']) . " ,";
9302 + iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
9305 + $res = DB_Helper::getInstance()->query($stmt);
9306 + if (PEAR::isError($res)) {
9307 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
9311 + $issue_id = DB_Helper::get_last_insert_id();
9317 + * Method used to get a specific parameter in the issue listing cookie.
9320 + * @param string $name The name of the parameter
9321 + * @return mixed The value of the specified parameter
9323 + function getParam($name)
9325 + $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
9327 + if (isset($_GET[$name])) {
9328 + return $_GET[$name];
9329 + } elseif (isset($_POST[$name])) {
9330 + return $_POST[$name];
9331 + } elseif (isset($profile[$name])) {
9332 + return $profile[$name];
9340 + * Method used to save the current search parameters in a cookie.
9343 + * @return array The search parameters
9345 + function saveSearchParams()
9347 + $sort_by = self::getParam('sort_by');
9348 + $sort_order = self::getParam('sort_order');
9349 + $users = self::getParam('users');
9350 + if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
9352 + unset($sort_order);
9354 + $rows = self::getParam('rows');
9355 + $hide_closed = self::getParam('hide_closed');
9356 + if ($hide_closed === '') {
9359 + $search_type = self::getParam('search_type');
9360 + if (empty($search_type)) {
9361 + $search_type = 'all_text';
9363 + $custom_field = self::getParam('custom_field');
9364 + if (is_string($custom_field)) {
9365 + $custom_field = unserialize(urldecode($custom_field));
9368 + 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
9369 + 'pagerRow' => self::getParam('pagerRow'),
9370 + 'hide_closed' => $hide_closed,
9371 + "sort_by" => $sort_by ? $sort_by : "pri_rank",
9372 + "sort_order" => $sort_order ? $sort_order : "ASC",
9373 + // quick filter form
9374 + 'keywords' => self::getParam('keywords'),
9375 + 'search_type' => $search_type,
9376 + 'users' => self::getParam('users'),
9377 + 'status' => self::getParam('status'),
9378 + 'priority' => self::getParam('priority'),
9379 + 'category' => self::getParam('category'),
9380 + 'customer_email' => self::getParam('customer_email'),
9381 + // advanced search form
9382 + 'show_authorized_issues' => self::getParam('show_authorized_issues'),
9383 + 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
9384 + 'reporter' => self::getParam('reporter'),
9386 + 'release' => self::getParam('release'),
9388 + 'custom_field' => $custom_field
9390 + // now do some magic to properly format the date fields
9391 + $date_fields = array(
9394 + 'last_response_date',
9395 + 'first_response_date',
9398 + foreach ($date_fields as $field_name) {
9399 + $field = self::getParam($field_name);
9400 + if (empty($field)) {
9403 + if (@$field['filter_type'] == 'in_past') {
9404 + @$cookie[$field_name] = array(
9405 + 'filter_type' => 'in_past',
9406 + 'time_period' => $field['time_period']
9409 + $end_field_name = $field_name . '_end';
9410 + $end_field = self::getParam($end_field_name);
9411 + @$cookie[$field_name] = array(
9412 + 'past_hour' => $field['past_hour'],
9413 + 'Year' => $field['Year'],
9414 + 'Month' => $field['Month'],
9415 + 'Day' => $field['Day'],
9416 + 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
9417 + 'filter_type' => $field['filter_type'],
9418 + 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
9420 + @$cookie[$end_field_name] = array(
9421 + 'Year' => $end_field['Year'],
9422 + 'Month' => $end_field['Month'],
9423 + 'Day' => $end_field['Day']
9427 + Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
9433 + * Method used to get the current sorting options used in the grid layout
9434 + * of the issue listing page.
9437 + * @param array $options The current search parameters
9438 + * @return array The sorting options
9440 + function getSortingInfo($options)
9443 + $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
9445 + // default order for last action date, priority should be descending
9446 + // for textual fields, like summary, ascending is reasonable
9448 + "pri_rank" => "desc",
9449 + "iss_id" => "desc",
9450 + "iss_customer_id" => "desc",
9451 + "prc_title" => "asc",
9452 + "sta_rank" => "asc",
9453 + "iss_created_date" => "desc",
9454 + "iss_summary" => "asc",
9455 + "last_action_date" => "desc",
9456 + "usr_full_name" => "asc",
9457 + "iss_expected_resolution_date" => "desc",
9458 + "pre_title" => "asc",
9459 + "assigned" => "asc",
9460 + "isu_order" => "desc",
9463 + foreach ($custom_fields as $fld_id => $fld_name) {
9464 + $fields['custom_field_' . $fld_id] = "desc";
9467 + $sortfields = array_combine(array_keys($fields), array_keys($fields));
9468 + $sortfields["pre_title"] = "pre_scheduled_date";
9469 + $sortfields["assigned"] = "isu_usr_id";
9472 + "links" => array(),
9473 + "images" => array()
9475 + foreach ($sortfields as $field => $sortfield) {
9476 + $sort_order = $fields[$field];
9477 + if ($options["sort_by"] == $sortfield) {
9478 + $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
9479 + if (strtolower($options["sort_order"]) == "asc") {
9480 + $sort_order = "desc";
9482 + $sort_order = "asc";
9485 + $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
9492 + * Returns the list of action date fields appropriate for the
9493 + * current user ID.
9496 + * @return array The list of action date fields
9498 + function getLastActionFields()
9500 + $last_action_fields = array(
9501 + "iss_last_public_action_date"
9503 + if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
9504 + $last_action_fields[] = "iss_last_internal_action_date";
9506 + if (count($last_action_fields) > 1) {
9507 + return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
9509 + return $last_action_fields[0] . " AS last_action_date";
9515 + * Method used to get the list of issues to be displayed in the grid layout.
9518 + * @param integer $prj_id The current project ID
9519 + * @param array $options The search parameters
9520 + * @param integer $current_row The current page number
9521 + * @param integer $max The maximum number of rows per page
9522 + * @return array The list of issues to be displayed
9524 + function getListing($prj_id, $options, $current_row = 0, $max = 5)
9526 + if (strtoupper($max) == "ALL") {
9529 + $start = $current_row * $max;
9530 + // get the current user's role
9531 + $usr_id = Auth::getUserID();
9532 + $role_id = User::getRoleByUser($usr_id, $prj_id);
9534 + // get any custom fields that should be displayed
9535 + $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
9543 + iss_customer_contract_id,
9546 + iss_last_response_date,
9548 + iss_last_customer_action_date,
9554 + sta_color status_color,
9559 + iss_last_public_action_date,
9560 + iss_last_public_action_type,
9561 + iss_last_internal_action_date,
9562 + iss_last_internal_action_type,
9563 + " . self::getLastActionFields() . ",
9564 + IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
9567 + iss_percent_complete,
9569 + iss_expected_resolution_date
9572 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
9573 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
9574 + // join custom fields if we are searching by custom fields
9575 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
9576 + foreach ($options['custom_field'] as $fld_id => $search_value) {
9577 + if (empty($search_value)) {
9580 + $field = Custom_Field::getDetails($fld_id);
9581 + if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
9584 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
9587 + if ($field['fld_type'] == 'multiple') {
9588 + $search_value = Misc::escapeInteger($search_value);
9589 + foreach ($search_value as $cfo_id) {
9590 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
9593 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
9598 + // check for the custom fields we want to sort by
9599 + if (strstr($options['sort_by'], 'custom_field') !== false) {
9600 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
9601 + $stmt .= "\n LEFT JOIN \n" .
9602 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
9604 + (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
9606 + if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
9609 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
9611 + isu_iss_id=iss_id";
9613 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
9616 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
9618 + iur_iss_id=iss_id";
9620 + if (!empty($options["show_notification_list_issues"])) {
9623 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
9625 + sub_iss_id=iss_id";
9629 + " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
9633 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
9637 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
9639 + iss_pre_id = pre_id
9641 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
9645 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
9649 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
9651 + iss_id=iqu_iss_id AND
9652 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
9654 + iss_prj_id= " . Misc::escapeInteger($prj_id);
9655 + $stmt .= self::buildWhereClause($options);
9657 + if (strstr($options["sort_by"], 'custom_field') !== false) {
9658 + $fld_details = Custom_Field::getDetails($fld_id);
9659 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
9661 + $sort_by = Misc::escapeString($options["sort_by"]);
9668 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
9670 + $total_rows = Pager::getTotalRows($stmt);
9673 + " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
9674 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
9675 + if (PEAR::isError($res)) {
9676 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
9682 + if (count($res) > 0) {
9683 + self::getAssignedUsersByIssues($res);
9684 + Time_Tracking::getTimeSpentByIssues($res);
9685 + // need to get the customer titles for all of these issues...
9686 + if (Customer::hasCustomerIntegration($prj_id)) {
9687 + Customer::getCustomerTitlesByIssues($prj_id, $res);
9688 + Customer::getSupportLevelsByIssues($prj_id, $res);
9690 + self::formatLastActionDates($res);
9691 + self::getLastStatusChangeDates($prj_id, $res);
9692 + } elseif ($current_row > 0) {
9693 + // if there are no results, and the page is not the first page reset page to one and reload results
9694 + Auth::redirect("list.php?pagerRow=0&rows=$max");
9696 + $groups = Group::getAssocList($prj_id);
9697 + $categories = Category::getAssocList($prj_id);
9698 + $column_headings = self::getColumnHeadings($prj_id);
9699 + if (count($custom_fields) > 0) {
9700 + $column_headings = array_merge($column_headings,$custom_fields);
9702 + $csv[] = @implode("\t", $column_headings);
9703 + for ($i = 0; $i < count($res); $i++) {
9704 + $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
9705 + $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
9706 + $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
9708 + $res[$i]['pri_title'],
9709 + $res[$i]['iss_id'],
9710 + $res[$i]['usr_full_name'],
9712 + // hide the group column from the output if no
9713 + // groups are available in the database
9714 + if (count($groups) > 0) {
9715 + $fields[] = $res[$i]['group'];
9717 + $fields[] = $res[$i]['assigned_users'];
9718 + $fields[] = $res[$i]['time_spent'];
9719 + // hide the category column from the output if no
9720 + // categories are available in the database
9721 + if (count($categories) > 0) {
9722 + $fields[] = $res[$i]['prc_title'];
9724 + if (Customer::hasCustomerIntegration($prj_id)) {
9725 + $fields[] = @$res[$i]['customer_title'];
9726 + // check if current user is acustomer and has a per incident contract.
9727 + // if so, check if issue is redeemed.
9728 + if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
9729 + if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
9730 + (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
9731 + $res[$i]['redeemed'] = true;
9735 + $fields[] = $res[$i]['sta_title'];
9736 + $fields[] = $res[$i]["status_change_date"];
9737 + $fields[] = $res[$i]["last_action_date"];
9738 + $fields[] = $res[$i]['iss_dev_time'];
9739 + $fields[] = $res[$i]['iss_summary'];
9740 + $fields[] = $res[$i]['iss_expected_resolution_date'];
9742 + if (count($custom_fields) > 0) {
9743 + $res[$i]['custom_field'] = array();
9744 + $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
9745 + foreach ($custom_field_values as $this_field) {
9746 + if (!empty($custom_fields[$this_field['fld_id']])) {
9747 + $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
9748 + $fields[] = $this_field['value'];
9753 + $csv[] = @implode("\t", $fields);
9755 + $total_pages = ceil($total_rows / $max);
9756 + $last_page = $total_pages - 1;
9760 + "current_page" => $current_row,
9761 + "start_offset" => $start,
9762 + "end_offset" => $start + count($res),
9763 + "total_rows" => $total_rows,
9764 + "total_pages" => $total_pages,
9765 + "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
9766 + "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
9767 + "last_page" => $last_page,
9768 + "custom_fields" => $custom_fields
9770 + "csv" => @implode("\n", $csv)
9777 + * Processes a result set to format the "Last Action Date" column.
9780 + * @param array $result The result set
9782 + function formatLastActionDates(&$result)
9784 + for ($i = 0; $i < count($result); $i++) {
9785 + if (($result[$i]['action_type'] == "internal") &&
9786 + (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
9787 + $label = $result[$i]["iss_last_internal_action_type"];
9788 + $last_date = $result[$i]["iss_last_internal_action_date"];
9790 + $label = $result[$i]["iss_last_public_action_type"];
9791 + $last_date = $result[$i]["iss_last_public_action_date"];
9793 + $date = new Date($last_date);
9794 + $current = new Date(Date_Helper::getCurrentDateGMT());
9795 + $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
9796 + Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
9802 + * Retrieves the last status change date for the given issue.
9805 + * @param integer $prj_id The project ID
9806 + * @param array $result The associative array of data
9807 + * @see self::getListing()
9809 + function getLastStatusChangeDates($prj_id, &$result)
9812 + for ($i = 0; $i < count($result); $i++) {
9813 + $ids[] = $result[$i]["iss_sta_id"];
9815 + if (count($ids) == 0) {
9818 + $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
9819 + for ($i = 0; $i < count($result); $i++) {
9820 + if (empty($result[$i]['iss_sta_id'])) {
9821 + $result[$i]['status_change_date'] = '';
9823 + list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
9824 + if ((empty($label)) || (empty($date_field_name))) {
9825 + $result[$i]['status_change_date'] = '';
9828 + $current = new Date(Date_Helper::getCurrentDateGMT());
9829 + $desc = "$label: %s ago";
9830 + $target_date = $result[$i][$date_field_name];
9831 + if (empty($target_date)) {
9832 + $result[$i]['status_change_date'] = '';
9835 + $date = new Date($target_date);
9836 + $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
9843 + * Method used to get the list of issues to be displayed in the grid layout.
9846 + * @param array $options The search parameters
9847 + * @return string The where clause
9849 + function buildWhereClause($options)
9851 + $usr_id = Auth::getUserID();
9852 + $prj_id = Auth::getCurrentProject();
9853 + $role_id = User::getRoleByUser($usr_id, $prj_id);
9855 + $stmt = ' AND iss_usr_id = usr_id';
9856 + if ($role_id == User::getRoleID('Customer')) {
9857 + $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
9858 + } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
9860 + iss_usr_id = $usr_id OR
9861 + iur_usr_id = $usr_id
9865 + if (!empty($options["users"])) {
9866 + $stmt .= " AND (\n";
9867 + if (stristr($options["users"], "grp") !== false) {
9868 + $chunks = explode(":", $options["users"]);
9869 + $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
9871 + if ($options['users'] == '-1') {
9872 + $stmt .= 'isu_usr_id IS NULL';
9873 + } elseif ($options['users'] == '-2') {
9874 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
9875 + } elseif ($options['users'] == '-3') {
9876 + $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
9877 + } elseif ($options['users'] == '-4') {
9878 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
9880 + $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
9885 + if (!empty($options["reporter"])) {
9886 + $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
9888 + if (!empty($options["show_authorized_issues"])) {
9889 + $stmt .= " AND (iur_usr_id=$usr_id)";
9891 + if (!empty($options["show_notification_list_issues"])) {
9892 + $stmt .= " AND (sub_usr_id=$usr_id)";
9894 + if (!empty($options["keywords"])) {
9895 + $stmt .= " AND (\n";
9896 + if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
9897 + $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
9898 + } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
9899 + // check if the user is trying to search by customer email
9900 + $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
9901 + if (count($customer_ids) > 0) {
9902 + $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
9904 + // no results, kill query
9905 + $stmt .= " iss_customer_id = -1";
9908 + $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
9909 + $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
9913 + if (!empty($options["priority"])) {
9914 + $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
9916 + if (!empty($options["status"])) {
9917 + $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
9919 + if (!empty($options["category"])) {
9920 + if (!is_array($options['category'])) {
9921 + $options['category'] = array($options['category']);
9923 + $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
9925 + if (!empty($options["hide_closed"])) {
9926 + $stmt .= " AND sta_is_closed=0";
9928 + if (!empty($options['release'])) {
9929 + $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
9931 + // now for the date fields
9932 + $date_fields = array(
9935 + 'last_response_date',
9936 + 'first_response_date',
9939 + foreach ($date_fields as $field_name) {
9940 + if (!empty($options[$field_name])) {
9941 + switch ($options[$field_name]['filter_type']) {
9943 + $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
9946 + $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
9949 + $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
9952 + $stmt .= " AND iss_$field_name IS NULL";
9955 + if (strlen($options[$field_name]['time_period']) == 0) {
9956 + $options[$field_name]['time_period'] = 0;
9958 + $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
9959 + Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
9965 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
9966 + foreach ($options['custom_field'] as $fld_id => $search_value) {
9967 + if (empty($search_value)) {
9970 + $field = Custom_Field::getDetails($fld_id);
9971 + $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
9972 + if (($field['fld_type'] == 'date') &&
9973 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
9976 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
9980 + if ($field['fld_type'] == 'multiple') {
9981 + $search_value = Misc::escapeInteger($search_value);
9982 + foreach ($search_value as $cfo_id) {
9983 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
9984 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
9985 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
9987 + } elseif ($field['fld_type'] == 'date') {
9988 + if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
9991 + $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
9992 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
9993 + cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
9994 + } else if ($field['fld_type'] == 'integer') {
9995 + $value = $search_value['value'];
9996 + switch ($search_value['filter_type']) {
9997 + case 'ge': $cmp = '>='; break;
9998 + case 'le': $cmp = '<='; break;
9999 + case 'gt': $cmp = '>'; break;
10000 + case 'lt': $cmp = '<'; break;
10001 + default: $cmp = '='; break;
10003 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
10004 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
10005 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
10007 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
10008 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
10009 + if ($field['fld_type'] == 'combo') {
10010 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
10012 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
10018 + // clear cached full-text values if we are not searching fulltext anymore
10019 + if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
10020 + Session::set('fulltext_string', '');
10021 + Session::set('fulltext_issues', '');
10028 + * Method used to get the previous and next issues that are available
10029 + * according to the current search parameters.
10032 + * @param integer $issue_id The issue ID
10033 + * @param array $options The search parameters
10034 + * @return array The list of issues
10036 + function getSides($issue_id, $options)
10038 + $usr_id = Auth::getUserID();
10039 + $role_id = Auth::getCurrentRole();
10043 + " . self::getLastActionFields() . "
10046 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10047 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
10048 + // join custom fields if we are searching by custom fields
10049 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
10050 + foreach ($options['custom_field'] as $fld_id => $search_value) {
10051 + if (empty($search_value)) {
10054 + $field = Custom_Field::getDetails($fld_id);
10055 + if (($field['fld_type'] == 'date') &&
10056 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
10059 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
10063 + if ($field['fld_type'] == 'multiple') {
10064 + $search_value = Misc::escapeInteger($search_value);
10065 + foreach ($search_value as $cfo_id) {
10066 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
10069 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
10074 + // check for the custom fields we want to sort by
10075 + if (strstr($options['sort_by'], 'custom_field') !== false) {
10076 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
10077 + $stmt .= "\n LEFT JOIN \n" .
10078 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
10080 + (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
10082 + if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
10085 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
10087 + isu_iss_id=iss_id";
10089 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
10092 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
10094 + iur_iss_id=iss_id";
10096 + if (!empty($options["show_notification_list_issues"])) {
10099 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
10101 + sub_iss_id=iss_id";
10103 + if (@$options["sort_by"] == "pre_scheduled_date") {
10106 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
10108 + iss_pre_id = pre_id";
10110 + if (@$options['sort_by'] == 'prc_title') {
10113 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
10115 + iss_prc_id = prc_id";
10119 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10121 + iss_sta_id=sta_id
10123 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
10125 + iss_pri_id=pri_id
10127 + iss_prj_id=" . Auth::getCurrentProject();
10128 + $stmt .= self::buildWhereClause($options);
10129 + if (strstr($options["sort_by"], 'custom_field') !== false) {
10130 + $fld_details = Custom_Field::getDetails($fld_id);
10131 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
10133 + $sort_by = Misc::escapeString($options["sort_by"]);
10139 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
10141 + $res = DB_Helper::getInstance()->getCol($stmt);
10142 + if (PEAR::isError($res)) {
10143 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10146 + // COMPAT: the next line requires PHP >= 4.0.5
10147 + $index = array_search($issue_id, $res);
10148 + if (!empty($res[$index+1])) {
10149 + $next = $res[$index+1];
10151 + if (!empty($res[$index-1])) {
10152 + $previous = $res[$index-1];
10155 + "next" => @$next,
10156 + "previous" => @$previous
10163 + * Method used to get the full list of user IDs assigned to a specific
10167 + * @param integer $issue_id The issue ID
10168 + * @return array The list of user IDs
10170 + function getAssignedUserIDs($issue_id)
10175 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10176 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10178 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
10179 + isu_usr_id=usr_id";
10180 + $res = DB_Helper::getInstance()->getCol($stmt);
10181 + if (PEAR::isError($res)) {
10182 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10191 + * Method used to see if a user is assigned to an issue.
10194 + * @param integer $issue_id The issue ID
10195 + * @param integer $usr_id An integer containg the ID of the user.
10196 + * @return boolean true if the user(s) are assigned to the issue.
10198 + function isAssignedToUser($issue_id, $usr_id)
10200 + $assigned_users = self::getAssignedUserIDs($issue_id);
10201 + if (in_array($usr_id, $assigned_users)) {
10210 + * Method used to get the full list of reporters associated with a given
10211 + * list of issues.
10214 + * @param array $result The result set
10217 + function getReportersByIssues(&$result)
10220 + for ($i = 0; $i < count($result); $i++) {
10221 + $ids[] = $result[$i]["iss_id"];
10223 + $ids = implode(", ", $ids);
10226 + CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
10228 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10229 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10231 + iss_usr_id=usr_id AND
10232 + iss_id IN ($ids)";
10233 + $res = DB_Helper::getInstance()->getAssoc($stmt);
10234 + if (PEAR::isError($res)) {
10235 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10237 + // now populate the $result variable again
10238 + for ($i = 0; $i < count($result); $i++) {
10239 + @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
10246 + * Method used to get the full list of assigned users by a list
10247 + * of issues. This was originally created to optimize the issue
10251 + * @param array $result The result set
10254 + function getAssignedUsersByIssues(&$result)
10257 + for ($i = 0; $i < count($result); $i++) {
10258 + $ids[] = $result[$i]["iss_id"];
10260 + if (count($ids) < 1) {
10263 + $ids = implode(", ", $ids);
10270 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10271 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10273 + isu_usr_id=usr_id AND
10274 + isu_iss_id IN ($ids)";
10275 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10276 + if (PEAR::isError($res)) {
10277 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10279 + // gather names of the users assigned to each issue
10281 + for ($i = 0; $i < count($res); $i++) {
10282 + if (!empty($t[$res[$i]['isu_iss_id']])) {
10283 + $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
10285 + $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
10290 + for ($i = 0; $i < count($res); $i++) {
10291 + if (empty($o[$res[$i]['isu_iss_id']])) {
10292 + $o[$res[$i]['isu_iss_id']] = array();
10294 + $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
10296 + // now populate the $result variable again
10297 + for ($i = 0; $i < count($result); $i++) {
10298 + @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
10299 + @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
10306 + * Method used to add the issue description to a list of issues.
10309 + * @param array $result The result set
10312 + function getDescriptionByIssues(&$result)
10314 + if (count($result) == 0) {
10319 + for ($i = 0; $i < count($result); $i++) {
10320 + $ids[] = $result[$i]["iss_id"];
10322 + $ids = implode(", ", $ids);
10328 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10330 + iss_id in ($ids)";
10331 + $res = DB_Helper::getInstance()->getAssoc($stmt);
10332 + if (PEAR::isError($res)) {
10333 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10335 + for ($i = 0; $i < count($result); $i++) {
10336 + @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
10343 + * Method used to get the full list of users (the full names) assigned to a
10344 + * specific issue.
10347 + * @param integer $issue_id The issue ID
10348 + * @return array The list of users
10350 + function getAssignedUsers($issue_id)
10355 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10356 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10358 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
10359 + isu_usr_id=usr_id";
10360 + $res = DB_Helper::getInstance()->getCol($stmt);
10361 + if (PEAR::isError($res)) {
10362 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10371 + * Method used to get the details for a specific issue.
10374 + * @param integer $issue_id The issue ID
10375 + * @param boolean $force_refresh If the cache should not be used.
10376 + * @return array The details for the specified issue
10378 + function getDetails($issue_id, $force_refresh = false)
10382 + $issue_id = Misc::escapeInteger($issue_id);
10384 + if (empty($issue_id)) {
10388 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
10389 + return $returns[$issue_id];
10393 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
10399 + sta_abbreviation,
10400 + sta_color status_color,
10404 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10405 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
10408 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
10410 + iss_pri_id=pri_id
10412 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10414 + iss_sta_id=sta_id
10416 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
10418 + iss_prc_id=prc_id
10420 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
10422 + iss_pre_id=pre_id
10424 + iss_id=$issue_id AND
10425 + iss_prj_id=prj_id";
10426 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10427 + if (PEAR::isError($res)) {
10428 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10431 + if (empty($res)) {
10434 + $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
10435 + // get customer information, if any
10436 + if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
10437 + $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
10438 + $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
10439 + $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
10440 + $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
10441 + $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
10442 + $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
10443 + if (empty($res['iss_first_response_date'])) {
10444 + $first_response_deadline = $created_date_ts + $max_first_response_time;
10445 + if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
10446 + $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
10448 + $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
10452 + $res['iss_original_description'] = $res["iss_description"];
10453 + if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
10454 + $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
10455 + $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
10457 + $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
10458 + $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
10459 + $res['iss_created_date_ts'] = $created_date_ts;
10460 + $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
10461 + list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
10462 + $temp = self::getAssignedUsersStatus($res["iss_id"]);
10463 + $res["has_inactive_users"] = 0;
10464 + $res["assigned_users"] = array();
10465 + $res["assigned_inactive_users"] = array();
10466 + foreach ($temp as $usr_id => $usr_status) {
10467 + if (!User::isActiveStatus($usr_status)) {
10468 + $res["assigned_inactive_users"][] = $usr_id;
10469 + $res["has_inactive_users"] = 1;
10471 + $res["assigned_users"][] = $usr_id;
10474 + if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
10475 + $res["is_current_user_assigned"] = 1;
10477 + $res["is_current_user_assigned"] = 0;
10479 + $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
10480 + $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
10481 + $res["reporter"] = User::getFullName($res["iss_usr_id"]);
10482 + if (empty($res["iss_updated_date"])) {
10483 + $res["iss_updated_date"] = 'not updated yet';
10485 + $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
10487 + $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
10488 + if (Release::isAssignable($res["iss_pre_id"])) {
10489 + $release = Release::getDetails($res["iss_pre_id"]);
10490 + $res["pre_title"] = $release["pre_title"];
10491 + $res["pre_status"] = $release["pre_status"];
10493 + // need to return the list of issues that are duplicates of this one
10494 + $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
10495 + $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
10496 + // also get the issue title of the duplicated issue
10497 + if (!empty($res['iss_duplicated_iss_id'])) {
10498 + $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
10501 + // get group information
10502 + if (!empty($res["iss_grp_id"])) {
10503 + $res["group"] = Group::getDetails($res["iss_grp_id"]);
10506 + // get quarantine issue
10507 + $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
10509 + $returns[$issue_id] = $res;
10517 + * Method used to get some simple details about the given duplicated issue.
10520 + * @param integer $issue_id The issue ID
10521 + * @return array The duplicated issue details
10523 + function getDuplicatedDetails($issue_id)
10526 + iss_summary title,
10527 + sta_title current_status,
10528 + sta_is_closed is_closed
10530 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10531 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10533 + iss_sta_id=sta_id AND
10534 + iss_id=$issue_id";
10535 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10536 + if (PEAR::isError($res)) {
10537 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10546 + * Method used to bulk update a list of issues
10549 + * @return boolean
10551 + function bulkUpdate()
10553 + // check if user performing this chance has the proper role
10554 + if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
10558 + $items = Misc::escapeInteger($_POST['item']);
10559 + $new_status_id = Misc::escapeInteger($_POST['status']);
10560 + $new_release_id = Misc::escapeInteger(@$_POST['release']);
10561 + $new_priority_id = Misc::escapeInteger($_POST['priority']);
10562 + $new_category_id = Misc::escapeInteger($_POST['category']);
10564 + for ($i = 0; $i < count($items); $i++) {
10565 + if (!self::canAccess($items[$i], Auth::getUserID())) {
10567 + } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
10568 + // make sure issue is not in another project
10572 + $updated_fields = array();
10574 + // update assignment
10575 + if (count(@$_POST['users']) > 0) {
10576 + $users = Misc::escapeInteger($_POST['users']);
10577 + // get who this issue is currently assigned too
10582 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10583 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10585 + isu_usr_id = usr_id AND
10586 + isu_iss_id = " . $items[$i];
10587 + $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
10588 + if (PEAR::isError($current_assignees)) {
10589 + Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
10592 + foreach ($current_assignees as $usr_id => $usr_name) {
10593 + if (!in_array($usr_id, $users)) {
10594 + self::deleteUserAssociation($items[$i], $usr_id, false);
10597 + $new_user_names = array();
10598 + $new_assignees = array();
10599 + foreach ($users as $usr_id) {
10600 + $new_user_names[$usr_id] = User::getFullName($usr_id);
10602 + // check if the issue is already assigned to this person
10604 + COUNT(*) AS total
10606 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
10608 + isu_iss_id=" . $items[$i] . " AND
10609 + isu_usr_id=" . $usr_id;
10610 + $total = DB_Helper::getInstance()->getOne($stmt);
10611 + if ($total > 0) {
10614 + $new_assignees[] = $usr_id;
10615 + // add the assignment
10616 + self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
10617 + Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
10618 + Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
10621 + Notification::notifyNewAssignment($new_assignees, $items[$i]);
10622 + $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
10626 + if (!empty($new_status_id)) {
10627 + $old_status_id = self::getStatusID($items[$i]);
10628 + $res = self::setStatus($items[$i], $new_status_id, false);
10630 + $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
10634 + // update release
10635 + if (!empty($new_release_id)) {
10636 + $old_release_id = self::getRelease($items[$i]);
10637 + $res = self::setRelease($items[$i], $new_release_id);
10639 + $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
10643 + // update priority
10644 + if (!empty($new_priority_id)) {
10645 + $old_priority_id = self::getPriority($items[$i]);
10646 + $res = self::setPriority($items[$i], $new_priority_id);
10648 + $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
10652 + // update category
10653 + if (!empty($new_category_id)) {
10654 + $old_category_id = self::getCategory($items[$i]);
10655 + $res = self::setCategory($items[$i], $new_category_id);
10657 + $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
10661 + if (count($updated_fields) > 0) {
10662 + // log the changes
10665 + foreach ($updated_fields as $key => $value) {
10667 + $changes .= "; ";
10669 + $changes .= "$key: $value";
10672 + History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
10675 + // close if request
10676 + if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
10677 + self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
10685 + * Method used to set the initial impact analysis for a specific issue
10688 + * @param integer $issue_id The issue ID
10689 + * @return integer 1 if the update worked, -1 otherwise
10691 + function setImpactAnalysis($issue_id)
10694 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10696 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
10697 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
10698 + iss_last_internal_action_type='update',
10699 + iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
10700 + iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
10702 + iss_id=" . Misc::escapeInteger($issue_id);
10703 + $res = DB_Helper::getInstance()->query($stmt);
10704 + if (PEAR::isError($res)) {
10705 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10708 + // add the impact analysis to the history of the issue
10709 + $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
10710 + History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
10717 + * Method used to get the full list of issue IDs that area available in the
10721 + * @param string $extra_condition An extra condition in the WHERE clause
10722 + * @return array The list of issue IDs
10724 + function getColList($extra_condition = NULL)
10729 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10731 + iss_prj_id=" . Auth::getCurrentProject();
10732 + if (!empty($extra_condition)) {
10733 + $stmt .= " AND $extra_condition ";
10738 + $res = DB_Helper::getInstance()->getCol($stmt);
10739 + if (PEAR::isError($res)) {
10740 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10749 + * Method used to get the full list of issue IDs and their respective
10753 + * @param string $extra_condition An extra condition in the WHERE clause
10754 + * @return array The list of issues
10756 + function getAssocList($extra_condition = NULL)
10762 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10764 + iss_prj_id=" . Auth::getCurrentProject();
10765 + if (!empty($extra_condition)) {
10766 + $stmt .= " AND $extra_condition ";
10771 + $res = DB_Helper::getInstance()->getAssoc($stmt);
10772 + if (PEAR::isError($res)) {
10773 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10782 + * Method used to get the list of issues associated to a specific issue.
10785 + * @param integer $issue_id The issue ID
10786 + * @return array The list of associated issues
10788 + function getAssociatedIssues($issue_id)
10790 + $issues = self::getAssociatedIssuesDetails($issue_id);
10791 + $associated = array();
10792 + for ($i = 0; $i < count($issues); $i++) {
10793 + $associated[] = $issues[$i]['associated_issue'];
10795 + return $associated;
10800 + * Method used to get the list of issues associated details to a
10801 + * specific issue.
10804 + * @param integer $issue_id The issue ID
10805 + * @return array The list of associated issues
10807 + function getAssociatedIssuesDetails($issue_id)
10811 + if (!empty($returns[$issue_id])) {
10812 + return $returns[$issue_id];
10816 + isa_associated_id associated_issue,
10817 + iss_summary associated_title,
10818 + sta_title current_status,
10819 + sta_is_closed is_closed
10821 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
10822 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10823 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10825 + isa_associated_id=iss_id AND
10826 + iss_sta_id=sta_id AND
10827 + isa_issue_id=$issue_id";
10828 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10829 + if (PEAR::isError($res)) {
10830 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10833 + $returns[$issue_id] = $res;
10840 + * Method used to check whether an issue was already closed or not.
10843 + * @param integer $issue_id The issue ID
10844 + * @return boolean
10846 + function isClosed($issue_id)
10851 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10852 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10854 + iss_id=" . Misc::escapeInteger($issue_id) . " AND
10855 + iss_sta_id=sta_id AND
10856 + sta_is_closed=1";
10857 + $res = DB_Helper::getInstance()->getOne($stmt);
10858 + if (PEAR::isError($res)) {
10859 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10872 + * Returns a simple list of issues that are currently set to some
10873 + * form of quarantine. This is mainly used by the IRC interface.
10876 + * @return array List of quarantined issues
10878 + function getQuarantinedIssueList()
10880 + // XXX: would be nice to restrict the result list to only one project
10885 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10886 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10888 + iqu_iss_id=iss_id AND
10889 + iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
10890 + iqu_expiration IS NOT NULL";
10891 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10892 + if (PEAR::isError($res)) {
10893 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10896 + self::getAssignedUsersByIssues($res);
10903 + * Returns the status of a quarantine.
10905 + * @param integer $issue_id The issue ID
10906 + * @return integer Indicates what the current state of quarantine is.
10908 + function getQuarantineInfo($issue_id)
10914 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10916 + iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
10917 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
10918 + iqu_expiration IS NULL)";
10919 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10920 + if (PEAR::isError($res)) {
10921 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10924 + if (!empty($res["iqu_expiration"])) {
10925 + $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
10926 + $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
10934 + * Sets the quarantine status. Optionally an expiration date can be set
10935 + * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
10938 + * @param integer $issue_id The issue ID
10939 + * @param integer $status The quarantine status
10940 + * @param string $expiration The expiration date of quarantine (default empty)
10942 + function setQuarantine($issue_id, $status, $expiration = '')
10944 + $issue_id = Misc::escapeInteger($issue_id);
10945 + $status = Misc::escapeInteger($status);
10947 + // see if there is an existing record
10951 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10953 + iqu_iss_id = $issue_id";
10954 + $res = DB_Helper::getInstance()->getOne($stmt);
10955 + if (PEAR::isError($res)) {
10956 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10962 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10964 + iqu_status = $status";
10965 + if (!empty($expiration)) {
10966 + $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
10968 + $stmt .= "\nWHERE
10969 + iqu_iss_id = $issue_id";
10970 + $res = DB_Helper::getInstance()->query($stmt);
10971 + if (PEAR::isError($res)) {
10972 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10975 + // add history entry about this change taking place
10976 + if ($status == 0) {
10977 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
10978 + "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
10983 + $stmt = "INSERT INTO
10984 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10988 + if (!empty($expiration)) {
10989 + $stmt .= ",\niqu_expiration\n";
10991 + $stmt .= ") VALUES (
10994 + if (!empty($expiration)) {
10995 + $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
10998 + $res = DB_Helper::getInstance()->query($stmt);
10999 + if (PEAR::isError($res)) {
11000 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11009 + * Sets the group of the issue.
11012 + * @param integer $issue_id The ID of the issue
11013 + * @param integer $group_id The ID of the group
11014 + * @return integer 1 if successful, -1 or -2 otherwise
11016 + function setGroup($issue_id, $group_id)
11018 + $issue_id = Misc::escapeInteger($issue_id);
11019 + $group_id = Misc::escapeInteger($group_id);
11021 + $current = self::getDetails($issue_id);
11022 + if ($current["iss_grp_id"] == $group_id) {
11026 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11028 + iss_grp_id = $group_id
11030 + iss_id = $issue_id";
11031 + $res = DB_Helper::getInstance()->query($stmt);
11032 + if (PEAR::isError($res)) {
11033 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11036 + $current_user = Auth::getUserID();
11037 + if (empty($current_user)) {
11038 + $current_user = APP_SYSTEM_USER_ID;
11040 + History::add($issue_id, $current_user, History::getTypeID('group_changed'),
11041 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
11047 + * Returns the group ID associated with the given issue ID.
11050 + * @param integer $issue_id The issue ID
11051 + * @return integer The associated group ID
11053 + function getGroupID($issue_id)
11058 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11060 + iss_id=" . Misc::escapeInteger($issue_id);
11061 + $res = DB_Helper::getInstance()->getOne($stmt);
11062 + if (PEAR::isError($res)) {
11063 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11072 + * Returns an array of issues based on full text search results.
11074 + * @param array $options An array of search options
11075 + * @return array An array of issue IDS
11077 + function getFullTextIssues($options)
11079 + // check if a list of issues for this full text search is already cached
11080 + $fulltext_string = Session::get('fulltext_string');
11081 + if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
11082 + return Session::get('fulltext_issues');
11085 + // no pre-existing list, generate them
11089 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11091 + MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11094 + DISTINCT(not_iss_id)
11096 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
11098 + MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11101 + DISTINCT(ttr_iss_id)
11103 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
11105 + MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11108 + DISTINCT(phs_iss_id)
11110 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
11112 + MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11115 + DISTINCT(sup_iss_id)
11117 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
11118 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
11120 + sup_id = seb_sup_id AND
11121 + MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11123 + $res = DB_Helper::getInstance()->getCol($stmt);
11124 + if (PEAR::isError($res)) {
11125 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11126 + return array(-1);
11129 + DISTINCT(icf_iss_id)
11131 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
11133 + MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
11134 + $custom_res = DB_Helper::getInstance()->getCol($stmt);
11135 + if (PEAR::isError($custom_res)) {
11136 + Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
11137 + return array(-1);
11139 + $issues = array_merge($res, $custom_res);
11140 + // we kill the query results on purpose to flag that no
11141 + // issues could be found with fulltext search
11142 + if (count($issues) < 1) {
11143 + $issues = array(-1);
11145 + Session::set('fulltext_string', $options['keywords']);
11146 + Session::set('fulltext_issues', $issues);
11153 + * Method to determine if user can access a particular issue
11156 + * @param integer $issue_id The ID of the issue.
11157 + * @param integer $usr_id The ID of the user
11158 + * @return boolean If the user can access the issue
11160 + function canAccess($issue_id, $usr_id)
11164 + if (empty($issue_id)) {
11168 + if (isset($access[$issue_id . "-" . $usr_id])) {
11169 + return $access[$issue_id . "-" . $usr_id];
11172 + $details = self::getDetails($issue_id);
11173 + if (empty($details)) {
11176 + $usr_details = User::getDetails($usr_id);
11177 + $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
11178 + $prj_id = self::getProjectID($issue_id);
11181 + if (empty($usr_role)) {
11182 + // check if they are even allowed to access the project
11184 + } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
11185 + ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
11186 + // check customer permissions
11188 + } elseif ($details['iss_private'] == 1) {
11189 + // check if the issue is even private
11191 + // check role, reporter, assigment and group
11192 + if ($usr_role > User::getRoleID("Developer")) {
11194 + } elseif ($details['iss_usr_id'] == $usr_id) {
11196 + } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
11198 + } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
11199 + ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
11201 + } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
11206 + } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
11207 + ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
11213 + $access[$issue_id . "-" . $usr_id] = $return;
11219 + * Returns true if the specified issue is private, false otherwise
11222 + * @param integer $issue_id The ID of the issue
11223 + * @return boolean If the issue is private or not
11225 + function isPrivate($issue_id)
11229 + if (!isset($returns[$issue_id])) {
11233 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11235 + iss_id=$issue_id";
11236 + $res = DB_Helper::getInstance()->getOne($sql);
11237 + if (PEAR::isError($res)) {
11238 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11242 + $returns[$issue_id] = true;
11244 + $returns[$issue_id] = false;
11248 + return $returns[$issue_id];
11253 + * Clears closed information from an issues.
11256 + * @param integer $issue_id The ID of the issue
11258 + function clearClosed($issue_id)
11261 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11263 + iss_closed_date = null,
11264 + iss_res_id = null
11266 + iss_id=" . Misc::escapeInteger($issue_id);
11267 + $res = DB_Helper::getInstance()->query($stmt);
11268 + if (PEAR::isError($res)) {
11269 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11272 + self::moveOrderForAllUsers($issue_id, 1);
11277 + * Returns the message ID that should be used as the parent ID for all messages
11280 + * @param integer $issue_id The ID of the issue
11282 + function getRootMessageID($issue_id)
11285 + iss_root_message_id
11287 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11289 + iss_id=" . Misc::escapeInteger($issue_id);
11290 + $res = DB_Helper::getInstance()->getOne($sql);
11291 + if (PEAR::isError($res)) {
11292 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11301 + * Returns the issue ID of the issue with the specified root message ID, or false
11303 + * @param string $msg_id The Message ID
11304 + * @return integer The ID of the issue
11306 + function getIssueByRootMessageID($msg_id)
11310 + if (!empty($returns[$msg_id])) {
11311 + return $returns[$msg_id];
11316 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11318 + iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
11319 + $res = DB_Helper::getInstance()->getOne($sql);
11320 + if (PEAR::isError($res)) {
11321 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11324 + if (empty($res)) {
11325 + $returns[$msg_id] = false;
11327 + $returns[$msg_id] = $res;
11329 + return $returns[$msg_id];
11334 + * Sets the assignees for the issue
11336 + * @param integer $issue_id
11337 + * @param array $assignees
11339 + function setAssignees($issue_id, $assignees)
11341 + if (!is_array($assignees)) {
11342 + $assignees = array();
11345 + // see if there is anything to change
11346 + $old_assignees = self::getAssignedUserIDs($issue_id);
11347 + if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
11351 + $old_assignee_names = self::getAssignedUsers($issue_id);
11353 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
11354 + // clear up the assignments for this issue, and then assign it to the current user
11355 + self::deleteUserAssociations($issue_id);
11356 + $assignee_names = array();
11357 + foreach ($assignees as $assignee) {
11358 + $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
11359 + if ($res == -1) {
11362 + $assignee_names[] = User::getFullName($assignee);
11363 + Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
11366 + Notification::notifyNewAssignment($assignees, $issue_id);
11368 + // save a history entry about this...
11369 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
11370 + "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
11374 + * Reorders user's issues as requested by user
11376 + * @param $usr_id User to be reordered
11377 + * @param $issue_id Issue or array of issues to be moved
11378 + * @param $neworder The new order of the issues
11381 + function reorderUserIssues($usr_id, $issue_id, $neworder)
11383 + if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
11386 + if (!is_numeric($usr_id) || !is_numeric($neworder)) {
11389 + $usr_id = Misc::escapeInteger($usr_id);
11390 + $issue_id = Misc::escapeInteger($issue_id);
11391 + $neworder = Misc::escapeInteger($neworder);
11392 + if (is_array($issue_id)) {
11393 + $issue_count = count($issue_id);
11394 + $issue_id_str = implode(", ", $issue_id);
11396 + $issue_count = 1;
11397 + $issue_id_str = $issue_id;
11398 + $issue_id = array($issue_id);
11400 + // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
11401 + $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
11402 + self::rearrangeDeleteUserAssociationOrderList($orderlist);
11403 + // move down the orders to free the "order space" needed
11405 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11407 + isu_order = isu_order + $issue_count
11409 + isu_usr_id = $usr_id AND
11410 + isu_order >= $neworder";
11411 + $res = DB_Helper::getInstance()->query($stmt);
11412 + if (PEAR::isError($res)) {
11413 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11416 + //update the order for the issues being moved
11418 + foreach ($issue_id as $iss_id) {
11420 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11422 + isu_order = " . ($neworder + $i) . "
11424 + isu_usr_id = $usr_id AND
11425 + isu_iss_id = $iss_id";
11426 + $res = DB_Helper::getInstance()->query($stmt);
11427 + if (PEAR::isError($res)) {
11428 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11437 + * Get users issue order list
11439 + * @param $user_id User
11440 + * @param $order_list Order of the issues
11443 + function getIssueOrderByUser($usr_id) {
11445 + if (!is_numeric($usr_id)) {
11450 + isu_iss_id, isu_order
11452 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11454 + isu_usr_id = " . $usr_id ;
11456 + $order_list = array();
11458 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
11460 + if (PEAR::isError($res)) {
11461 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11464 + foreach ($res as $row) {
11465 + $order_list[$row["isu_iss_id"]] = $row["isu_order"];
11468 + return $order_list;
11471 + function moveOrderForAllUsers($issue_id, $neworder)
11473 + // Move the issue to the top priority for the ppl it's assigned to
11474 + $stmt = "SELECT isu_usr_id FROM
11475 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11477 + isu_iss_id = " . Misc::escapeInteger($issue_id);
11478 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
11479 + if (PEAR::isError($res)) {
11480 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11483 + foreach ($res as $row) {
11484 + self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
11489 --- eventum-2.2/lib/eventum/class.issue.php.~3~ 1970-01-01 02:00:00.000000000 +0200
11490 +++ eventum-2.2-order/lib/eventum/class.issue.php.~3~ 2009-10-12 22:10:36.442518536 +0300
11493 +/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
11494 +// +----------------------------------------------------------------------+
11495 +// | Eventum - Issue Tracking System |
11496 +// +----------------------------------------------------------------------+
11497 +// | Copyright (c) 2003 - 2008 MySQL AB |
11498 +// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
11500 +// | This program is free software; you can redistribute it and/or modify |
11501 +// | it under the terms of the GNU General Public License as published by |
11502 +// | the Free Software Foundation; either version 2 of the License, or |
11503 +// | (at your option) any later version. |
11505 +// | This program is distributed in the hope that it will be useful, |
11506 +// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11507 +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11508 +// | GNU General Public License for more details. |
11510 +// | You should have received a copy of the GNU General Public License |
11511 +// | along with this program; if not, write to: |
11513 +// | Free Software Foundation, Inc. |
11514 +// | 59 Temple Place - Suite 330 |
11515 +// | Boston, MA 02111-1307, USA. |
11516 +// +----------------------------------------------------------------------+
11517 +// | Authors: João Prado Maia <jpm@mysql.com> |
11518 +// +----------------------------------------------------------------------+
11523 + * Class designed to handle all business logic related to the issues in the
11524 + * system, such as adding or updating them or listing them in the grid mode.
11526 + * @author João Prado Maia <jpm@mysql.com>
11527 + * @version $Revision$
11533 + * Method used to check whether a given issue ID exists or not.
11536 + * @param integer $issue_id The issue ID
11537 + * @param boolean $check_project If we should check that this issue is in the current project
11538 + * @return boolean
11540 + function exists($issue_id, $check_project = true)
11545 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11547 + iss_id=" . Misc::escapeInteger($issue_id);
11548 + if ($check_project) {
11550 + iss_prj_id = " . Auth::getCurrentProject();
11552 + $res = DB_Helper::getInstance()->getOne($stmt);
11553 + if (PEAR::isError($res)) {
11554 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11567 + * Method used to get the list of column heading titles for the
11568 + * CSV export functionality of the issue listing screen.
11571 + * @param integer $prj_id The project ID
11572 + * @return array The list of column heading titles
11574 + function getColumnHeadings($prj_id)
11576 + $headings = array(
11581 + // hide the group column from the output if no
11582 + // groups are available in the database
11583 + $groups = Group::getAssocList($prj_id);
11584 + if (count($groups) > 0) {
11585 + $headings[] = 'Group';
11587 + $headings[] = 'Assigned';
11588 + $headings[] = 'Time Spent';
11589 + // hide the category column from the output if no
11590 + // categories are available in the database
11591 + $categories = Category::getAssocList($prj_id);
11592 + if (count($categories) > 0) {
11593 + $headings[] = 'Category';
11595 + if (Customer::hasCustomerIntegration($prj_id)) {
11596 + $headings[] = 'Customer';
11598 + $headings[] = 'Status';
11599 + $headings[] = 'Status Change Date';
11600 + $headings[] = 'Last Action Date';
11601 + $headings[] = 'Est. Dev. TIme';
11602 + $headings[] = 'Summary';
11603 + $headings[] = 'Expected Resolution Date';
11604 + return $headings;
11609 + * Method used to get the full list of date fields available to issues, to
11610 + * be used when customizing the issue listing screen in the 'last status
11611 + * change date' column.
11614 + * @param boolean $display_customer_fields Whether to include any customer related fields or not
11615 + * @return array The list of available date fields
11617 + function getDateFieldsAssocList($display_customer_fields = FALSE)
11620 + 'iss_created_date' => 'Created Date',
11621 + 'iss_updated_date' => 'Last Updated Date',
11622 + 'iss_last_response_date' => 'Last Response Date',
11623 + 'iss_closed_date' => 'Closed Date'
11625 + if ($display_customer_fields) {
11626 + $fields['iss_last_customer_action_date'] = 'Customer Action Date';
11634 + * Method used to get the full list of issue IDs and their respective
11635 + * titles associated to a given project.
11638 + * @param integer $prj_id The project ID
11639 + * @return array The list of issues
11641 + function getAssocListByProject($prj_id)
11647 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11649 + iss_prj_id=" . Misc::escapeInteger($prj_id) . "
11652 + $res = DB_Helper::getInstance()->getAssoc($stmt);
11653 + if (PEAR::isError($res)) {
11654 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11663 + * Method used to get the status of a given issue.
11666 + * @param integer $issue_id The issue ID
11667 + * @return integer The status ID
11669 + function getStatusID($issue_id)
11673 + $issue_id = Misc::escapeInteger($issue_id);
11675 + if (!empty($returns[$issue_id])) {
11676 + return $returns[$issue_id];
11682 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11684 + iss_id=$issue_id";
11685 + $res = DB_Helper::getInstance()->getOne($stmt);
11686 + if (PEAR::isError($res)) {
11687 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11690 + $returns[$issue_id] = $res;
11697 + * Records the last customer action date for a given issue ID.
11700 + * @param integer $issue_id The issue ID
11701 + * @return integer 1 if the update worked, -1 otherwise
11703 + function recordLastCustomerAction($issue_id)
11706 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11708 + iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11709 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11710 + iss_last_public_action_type='customer action'
11712 + iss_id=" . Misc::escapeInteger($issue_id);
11713 + $res = DB_Helper::getInstance()->query($stmt);
11714 + if (PEAR::isError($res)) {
11715 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11724 + * Returns the customer ID associated with the given issue ID.
11727 + * @param integer $issue_id The issue ID
11728 + * @return integer The customer ID associated with the issue
11730 + function getCustomerID($issue_id)
11734 + $issue_id = Misc::escapeInteger($issue_id);
11736 + if (!empty($returns[$issue_id])) {
11737 + return $returns[$issue_id];
11743 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11745 + iss_id=$issue_id";
11746 + $res = DB_Helper::getInstance()->getOne($stmt);
11747 + if (PEAR::isError($res)) {
11748 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11751 + $returns[$issue_id] = $res;
11758 + * Returns the contract ID associated with the given issue ID.
11761 + * @param integer $issue_id The issue ID
11762 + * @return integer The customer ID associated with the issue
11764 + function getContractID($issue_id)
11768 + $issue_id = Misc::escapeInteger($issue_id);
11770 + if (!empty($returns[$issue_id])) {
11771 + return $returns[$issue_id];
11775 + iss_customer_contract_id
11777 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11779 + iss_id=$issue_id";
11780 + $res = DB_Helper::getInstance()->getOne($stmt);
11781 + if (PEAR::isError($res)) {
11782 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11785 + $returns[$issue_id] = $res;
11792 + * Sets the contract ID for a specific issue.
11795 + * @param integer $issue_id The issue ID
11796 + * @param integer The contract ID
11797 + * @return integer 1 if the update worked, -1 otherwise
11799 + function setContractID($issue_id, $contract_id)
11801 + $issue_id = Misc::escapeInteger($issue_id);
11803 + $old_contract_id = self::getContractID($issue_id);
11806 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11808 + iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
11810 + iss_id=$issue_id";
11811 + $res = DB_Helper::getInstance()->query($stmt);
11812 + if (PEAR::isError($res)) {
11813 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11817 + History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
11824 + * Returns the customer ID associated with the given issue ID.
11827 + * @param integer $issue_id The issue ID
11828 + * @return integer The customer ID associated with the issue
11830 + function getContactID($issue_id)
11834 + $issue_id = Misc::escapeInteger($issue_id);
11836 + if (!empty($returns[$issue_id])) {
11837 + return $returns[$issue_id];
11841 + iss_customer_contact_id
11843 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11845 + iss_id=$issue_id";
11846 + $res = DB_Helper::getInstance()->getOne($stmt);
11847 + if (PEAR::isError($res)) {
11848 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11851 + $returns[$issue_id] = $res;
11858 + * Method used to get the project associated to a given issue.
11861 + * @param integer $issue_id The issue ID
11862 + * @param boolean $force_refresh If the cache should not be used.
11863 + * @return integer The project ID
11865 + function getProjectID($issue_id, $force_refresh = false)
11869 + $issue_id = Misc::escapeInteger($issue_id);
11871 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
11872 + return $returns[$issue_id];
11878 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11880 + iss_id=$issue_id";
11881 + $res = DB_Helper::getInstance()->getOne($stmt);
11882 + if (PEAR::isError($res)) {
11883 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11886 + $returns[$issue_id] = $res;
11893 + * Method used to remotely assign a given issue to an user.
11896 + * @param integer $issue_id The issue ID
11897 + * @param integer $usr_id The user ID of the person performing the change
11898 + * @param boolean $assignee The user ID of the assignee
11899 + * @return integer The status ID
11901 + function remoteAssign($issue_id, $usr_id, $assignee)
11903 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
11904 + // clear up the assignments for this issue, and then assign it to the current user
11905 + self::deleteUserAssociations($issue_id, $usr_id);
11906 + $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
11907 + if ($res != -1) {
11908 + // save a history entry about this...
11909 + History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
11910 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
11911 + if ($assignee != $usr_id) {
11912 + Notification::notifyNewAssignment(array($assignee), $issue_id);
11920 + * Method used to set the status of a given issue.
11923 + * @param integer $issue_id The issue ID
11924 + * @param integer $status_id The new status ID
11925 + * @param boolean $notify If a notification should be sent about this change.
11926 + * @return integer 1 if the update worked, -1 otherwise
11928 + function setStatus($issue_id, $status_id, $notify = false)
11930 + $issue_id = Misc::escapeInteger($issue_id);
11931 + $status_id = Misc::escapeInteger($status_id);
11933 + $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
11934 + if ($workflow !== true) {
11935 + return $workflow;
11938 + // check if the status is already set to the 'new' one
11939 + if (self::getStatusID($issue_id) == $status_id) {
11943 + $old_status = self::getStatusID($issue_id);
11944 + $old_details = Status::getDetails($old_status);
11947 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11949 + iss_sta_id=$status_id,
11950 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
11951 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11952 + iss_last_public_action_type='update'
11954 + iss_id=$issue_id";
11955 + $res = DB_Helper::getInstance()->query($stmt);
11956 + if (PEAR::isError($res)) {
11957 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11960 + // clear out the last-triggered-reminder flag when changing the status of an issue
11961 + Reminder_Action::clearLastTriggered($issue_id);
11963 + // if old status was closed and new status is not, clear closed data from issue.
11964 + if (@$old_details['sta_is_closed'] == 1) {
11965 + $new_details = Status::getDetails($status_id);
11966 + if ($new_details['sta_is_closed'] != 1) {
11967 + self::clearClosed($issue_id);
11972 + Notification::notifyStatusChange($issue_id, $old_status, $status_id);
11981 + * Method used to remotely set the status of a given issue.
11984 + * @param integer $issue_id The issue ID
11985 + * @param integer $usr_id The user ID of the person performing this change
11986 + * @param integer $new_status The new status ID
11987 + * @return integer 1 if the update worked, -1 otherwise
11989 + function setRemoteStatus($issue_id, $usr_id, $new_status)
11991 + $sta_id = Status::getStatusID($new_status);
11993 + $res = self::setStatus($issue_id, $sta_id);
11995 + // record history entry
11996 + History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
12003 + * Method used to set the release of an issue
12006 + * @param integer $issue_id The ID of the issue
12007 + * @param integer $pre_id The ID of the release to set this issue too
12008 + * @return integer 1 if the update worked, -1 otherwise
12010 + function setRelease($issue_id, $pre_id)
12012 + $issue_id = Misc::escapeInteger($issue_id);
12013 + $pre_id = Misc::escapeInteger($pre_id);
12015 + if ($pre_id != self::getRelease($issue_id)) {
12017 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12019 + iss_pre_id = $pre_id
12021 + iss_id = $issue_id";
12022 + $res = DB_Helper::getInstance()->query($sql);
12023 + if (PEAR::isError($res)) {
12024 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12034 + * Returns the current release of an issue
12037 + * @param integer $issue_id The ID of the issue
12038 + * @return integer The release
12040 + function getRelease($issue_id)
12045 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12047 + iss_id = " . Misc::escapeInteger($issue_id);
12048 + $res = DB_Helper::getInstance()->getOne($sql);
12049 + if (PEAR::isError($res)) {
12050 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12059 + * Method used to set the priority of an issue
12062 + * @param integer $issue_id The ID of the issue
12063 + * @param integer $pri_id The ID of the priority to set this issue too
12064 + * @return integer 1 if the update worked, -1 otherwise
12066 + function setPriority($issue_id, $pri_id)
12068 + $issue_id = Misc::escapeInteger($issue_id);
12069 + $pri_id = Misc::escapeInteger($pri_id);
12071 + if ($pri_id != self::getPriority($issue_id)) {
12073 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12075 + iss_pri_id = $pri_id
12077 + iss_id = $issue_id";
12078 + $res = DB_Helper::getInstance()->query($sql);
12079 + if (PEAR::isError($res)) {
12080 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12090 + * Returns the current issue priority
12093 + * @param integer $issue_id The ID of the issue
12094 + * @return integer The priority
12096 + function getPriority($issue_id)
12101 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12103 + iss_id = " . Misc::escapeInteger($issue_id);
12104 + $res = DB_Helper::getInstance()->getOne($sql);
12105 + if (PEAR::isError($res)) {
12106 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12115 + * Method used to set the category of an issue
12118 + * @param integer $issue_id The ID of the issue
12119 + * @param integer $prc_id The ID of the category to set this issue too
12120 + * @return integer 1 if the update worked, -1 otherwise
12122 + function setCategory($issue_id, $prc_id)
12124 + $issue_id = Misc::escapeInteger($issue_id);
12125 + $prc_id = Misc::escapeInteger($prc_id);
12127 + if ($prc_id != self::getPriority($issue_id)) {
12129 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12131 + iss_prc_id = $prc_id
12133 + iss_id = $issue_id";
12134 + $res = DB_Helper::getInstance()->query($sql);
12135 + if (PEAR::isError($res)) {
12136 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12146 + * Returns the current issue category
12149 + * @param integer $issue_id The ID of the issue
12150 + * @return integer The category
12152 + function getCategory($issue_id)
12157 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12159 + iss_id = " . Misc::escapeInteger($issue_id);
12160 + $res = DB_Helper::getInstance()->getOne($sql);
12161 + if (PEAR::isError($res)) {
12162 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12171 + * Method used to get all issues associated with a status that doesn't have
12172 + * the 'closed' context.
12175 + * @param integer $prj_id The project ID to list issues from
12176 + * @param integer $usr_id The user ID of the user requesting this information
12177 + * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
12178 + * @param integer $status_id The status ID to be used to restrict results
12179 + * @return array The list of open issues
12181 + function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
12183 + $prj_id = Misc::escapeInteger($prj_id);
12184 + $status_id = Misc::escapeInteger($status_id);
12185 + $projects = Project::getRemoteAssocListByUser($usr_id);
12186 + if (@count($projects) == 0) {
12196 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12197 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
12200 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
12202 + isu_iss_id=iss_id
12204 + if (!empty($status_id)) {
12205 + $stmt .= " sta_id=$status_id AND ";
12208 + iss_prj_id=$prj_id AND
12209 + sta_id=iss_sta_id AND
12210 + sta_is_closed=0";
12211 + if ($show_all_issues == false) {
12213 + isu_usr_id=$usr_id";
12215 + $stmt .= "\nGROUP BY
12217 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
12218 + if (PEAR::isError($res)) {
12219 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12222 + if (count($res) > 0) {
12223 + self::getAssignedUsersByIssues($res);
12231 + * Method used to build the required parameters to simulate an email reply
12232 + * to the user who reported the issue, using the issue details like summary
12233 + * and description as email fields.
12236 + * @param integer $issue_id The issue ID
12237 + * @return array The email parameters
12239 + function getReplyDetails($issue_id)
12241 + $issue_id = Misc::escapeInteger($issue_id);
12244 + iss_created_date,
12245 + usr_full_name AS reporter,
12246 + usr_email AS reporter_email,
12247 + iss_description AS description,
12248 + iss_summary AS sup_subject
12250 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12251 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
12253 + iss_usr_id=usr_id AND
12254 + iss_id=$issue_id";
12255 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
12256 + if (PEAR::isError($res)) {
12257 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12260 + $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
12261 + $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
12268 + * Method used to record the last updated timestamp for a given
12272 + * @param integer $issue_id The issue ID
12273 + * @param string $type The type of update that was made (optional)
12274 + * @return boolean
12276 + function markAsUpdated($issue_id, $type = false)
12278 + $public = array("staff response", "customer action", "file uploaded", "user response");
12280 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12282 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
12283 + if ($type != false) {
12284 + if (in_array($type, $public)) {
12285 + $field = "iss_last_public_action_";
12287 + $field = "iss_last_internal_action_";
12289 + $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
12290 + $field . "type ='" . Misc::escapeString($type) . "'\n";
12293 + iss_id=" . Misc::escapeInteger($issue_id);
12294 + $res = DB_Helper::getInstance()->query($stmt);
12295 + if (PEAR::isError($res)) {
12296 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12299 + // update last response dates if this is a staff response
12300 + if ($type == "staff response") {
12302 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12304 + iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
12306 + iss_id = " . Misc::escapeInteger($issue_id);
12307 + DB_Helper::getInstance()->query($stmt);
12309 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12311 + iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
12313 + iss_first_response_date IS NULL AND
12314 + iss_id = " . Misc::escapeInteger($issue_id);
12315 + DB_Helper::getInstance()->query($stmt);
12324 + * Method used to check whether a given issue has duplicates
12328 + * @param integer $issue_id The issue ID
12329 + * @return boolean
12331 + function hasDuplicates($issue_id)
12336 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12338 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
12339 + $res = DB_Helper::getInstance()->getOne($stmt);
12340 + if (PEAR::isError($res)) {
12341 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12354 + * Method used to update the duplicated issues for a given
12358 + * @param integer $issue_id The issue ID
12359 + * @return integer 1 if the update worked, -1 otherwise
12361 + function updateDuplicates($issue_id)
12363 + $issue_id = Misc::escapeInteger($issue_id);
12365 + $ids = self::getDuplicateList($issue_id);
12366 + if ($ids == '') {
12369 + $ids = @array_keys($ids);
12371 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12373 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12374 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12375 + iss_last_internal_action_type='updated',
12376 + iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
12377 + if (@$_POST["keep"] == "no") {
12378 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
12381 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
12382 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
12383 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
12385 + iss_id IN (" . implode(", ", $ids) . ")";
12386 + $res = DB_Helper::getInstance()->query($stmt);
12387 + if (PEAR::isError($res)) {
12388 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12391 + // record the change
12392 + for ($i = 0; $i < count($ids); $i++) {
12393 + History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
12394 + "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
12402 + * Method used to get a list of the duplicate issues for a given
12406 + * @param integer $issue_id The issue ID
12407 + * @return array The list of duplicates
12409 + function getDuplicateList($issue_id)
12411 + $res = self::getDuplicateDetailsList($issue_id);
12412 + if (@count($res) == 0) {
12416 + for ($i = 0; $i < count($res); $i++) {
12417 + $list[$res[$i]['issue_id']] = $res[$i]['title'];
12425 + * Method used to get a list of the duplicate issues (and their details)
12426 + * for a given issue ID.
12429 + * @param integer $issue_id The issue ID
12430 + * @return array The list of duplicates
12432 + function getDuplicateDetailsList($issue_id)
12436 + if (!empty($returns[$issue_id])) {
12437 + return $returns[$issue_id];
12442 + iss_summary title,
12443 + sta_title current_status,
12444 + sta_is_closed is_closed
12446 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12447 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
12449 + iss_sta_id=sta_id AND
12450 + iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
12451 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
12452 + if (PEAR::isError($res)) {
12453 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12456 + $returns[$issue_id] = $res;
12463 + * Method used to clear the duplicate status of an issue.
12466 + * @param integer $issue_id The issue ID
12467 + * @return integer 1 if the update worked, -1 otherwise
12469 + function clearDuplicateStatus($issue_id)
12471 + $issue_id = Misc::escapeInteger($issue_id);
12473 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12475 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12476 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12477 + iss_last_internal_action_type='updated',
12478 + iss_duplicated_iss_id=NULL
12480 + iss_id=$issue_id";
12481 + $res = DB_Helper::getInstance()->query($stmt);
12482 + if (PEAR::isError($res)) {
12483 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12486 + // record the change
12487 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
12494 + * Method used to mark an issue as a duplicate of an existing one.
12497 + * @param integer $issue_id The issue ID
12498 + * @return integer 1 if the update worked, -1 otherwise
12500 + function markAsDuplicate($issue_id)
12502 + $issue_id = Misc::escapeInteger($issue_id);
12503 + if (!self::exists($issue_id)) {
12508 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12510 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12511 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12512 + iss_last_internal_action_type='updated',
12513 + iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
12515 + iss_id=$issue_id";
12516 + $res = DB_Helper::getInstance()->query($stmt);
12517 + if (PEAR::isError($res)) {
12518 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12521 + if (!empty($_POST["comments"])) {
12522 + // add note with the comments of marking an issue as a duplicate of another one
12523 + $_POST['title'] = 'Issue duplication comments';
12524 + $_POST["note"] = $_POST["comments"];
12525 + Note::insert(Auth::getUserID(), $issue_id);
12527 + // record the change
12528 + History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
12529 + "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
12535 + function isDuplicate($issue_id)
12540 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12542 + iss_id = " . Misc::escapeInteger($issue_id) . " AND
12543 + iss_duplicated_iss_id IS NULL";
12544 + $res = DB_Helper::getInstance()->getOne($sql);
12545 + if (PEAR::isError($res)) {
12546 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12558 + * Method used to get an associative array of user ID => user
12559 + * status associated with a given issue ID.
12562 + * @param integer $issue_id The issue ID
12563 + * @return array The list of users
12565 + function getAssignedUsersStatus($issue_id)
12571 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
12572 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
12574 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
12575 + isu_usr_id=usr_id";
12576 + $res = DB_Helper::getInstance()->getAssoc($stmt);
12577 + if (PEAR::isError($res)) {
12578 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12587 + * Method used to get the summary associated with a given issue ID.
12590 + * @param integer $issue_id The issue ID
12591 + * @return string The issue summary
12593 + function getTitle($issue_id)
12598 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12600 + iss_id=" . Misc::escapeInteger($issue_id);
12601 + $res = DB_Helper::getInstance()->getOne($stmt);
12602 + if (PEAR::isError($res)) {
12603 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12612 + * Method used to get the issue ID associated with a specific summary.
12615 + * @param string $summary The summary to look for
12616 + * @return integer The issue ID
12618 + function getIssueID($summary)
12623 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12625 + iss_summary='" . Misc::escapeString($summary) . "'";
12626 + $res = DB_Helper::getInstance()->getOne($stmt);
12627 + if (PEAR::isError($res)) {
12628 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12631 + if (empty($res)) {
12641 + * Method used to add a new anonymous based issue in the system.
12644 + * @return integer The new issue ID
12646 + function addAnonymousReport()
12648 + $options = Project::getAnonymousPostOptions($_POST["project"]);
12649 + $initial_status = Project::getInitialStatus($_POST["project"]);
12650 + $stmt = "INSERT INTO
12651 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12658 + if (!empty($initial_status)) {
12659 + $stmt .= "iss_sta_id,";
12662 + iss_created_date,
12663 + iss_last_public_action_date,
12664 + iss_last_public_action_type,
12667 + iss_root_message_id
12669 + " . Misc::escapeInteger($_POST["project"]) . ",
12670 + " . $options["category"] . ",
12672 + " . $options["priority"] . ",
12673 + " . $options["reporter"] . ",";
12674 + if (!empty($initial_status)) {
12675 + $stmt .= "$initial_status,";
12678 + '" . Date_Helper::getCurrentDateGMT() . "',
12679 + '" . Date_Helper::getCurrentDateGMT() . "',
12681 + '" . Misc::escapeString($_POST["summary"]) . "',
12682 + '" . Misc::escapeString($_POST["description"]) . "',
12683 + '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
12685 + $res = DB_Helper::getInstance()->query($stmt);
12686 + if (PEAR::isError($res)) {
12687 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12690 + $new_issue_id = DB_Helper::get_last_insert_id();
12691 + // log the creation of the issue
12692 + History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
12694 + // now process any files being uploaded
12696 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
12697 + if (!@empty($_FILES["file"]["name"][$i])) {
12703 + $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
12704 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
12705 + $filename = @$_FILES["file"]["name"][$i];
12706 + if (empty($filename)) {
12709 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
12710 + if (!empty($blob)) {
12711 + Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
12715 + // need to process any custom fields ?
12716 + if (@count($_POST["custom_fields"]) > 0) {
12717 + foreach ($_POST["custom_fields"] as $fld_id => $value) {
12718 + Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
12722 + // now add the user/issue association
12723 + $assign = array();
12724 + $users = @$options["users"];
12725 + $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
12726 + for ($i = 0; $i < count($users); $i++) {
12727 + Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
12728 + self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
12729 + $assign[] = $users[$i];
12732 + Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
12734 + // also notify any users that want to receive emails anytime a new issue is created
12735 + Notification::notifyNewIssue($_POST['project'], $new_issue_id);
12737 + return $new_issue_id;
12743 + * Method used to remove all issues associated with a specific list of
12747 + * @param array $ids The list of projects to look for
12748 + * @return boolean
12750 + function removeByProjects($ids)
12752 + $items = @implode(", ", Misc::escapeInteger($ids));
12756 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12758 + iss_prj_id IN ($items)";
12759 + $res = DB_Helper::getInstance()->getCol($stmt);
12760 + if (PEAR::isError($res)) {
12761 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12764 + if (count($res) > 0) {
12765 + self::deleteAssociations($res);
12766 + Attachment::removeByIssues($res);
12767 + SCM::removeByIssues($res);
12768 + Impact_Analysis::removeByIssues($res);
12769 + self::deleteUserAssociations($res);
12770 + Note::removeByIssues($res);
12771 + Time_Tracking::removeByIssues($res);
12772 + Notification::removeByIssues($res);
12773 + Custom_Field::removeByIssues($res);
12774 + Phone_Support::removeByIssues($res);
12775 + History::removeByIssues($res);
12776 + // now really delete the issues
12777 + $items = implode(", ", $res);
12778 + $stmt = "DELETE FROM
12779 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12781 + iss_id IN ($items)";
12782 + DB_Helper::getInstance()->query($stmt);
12790 + * Method used to close off an issue.
12793 + * @param integer $usr_id The user ID
12794 + * @param integer $issue_id The issue ID
12795 + * @param bool $send_notification Whether to send a notification about this action or not
12796 + * @param integer $resolution_id The resolution ID
12797 + * @param integer $status_id The status ID
12798 + * @param string $reason The reason for closing this issue
12799 + * @param string $send_notification_to Who this notification should be sent too
12800 + * @return integer 1 if the update worked, -1 otherwise
12802 + function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
12804 + $usr_id = Misc::escapeInteger($usr_id);
12805 + $issue_id = Misc::escapeInteger($issue_id);
12806 + $resolution_id = Misc::escapeInteger($resolution_id);
12807 + $status_id = Misc::escapeInteger($status_id);
12810 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12812 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12813 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12814 + iss_last_public_action_type='closed',
12815 + iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
12816 + if (!empty($resolution_id)) {
12817 + $stmt .= "iss_res_id=$resolution_id,\n";
12819 + $stmt .= "iss_sta_id=$status_id
12821 + iss_id=$issue_id";
12822 + $res = DB_Helper::getInstance()->query($stmt);
12823 + if (PEAR::isError($res)) {
12824 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12827 + self::moveOrderForAllUsers($issue_id, 1000);
12828 + $prj_id = self::getProjectID($issue_id);
12830 + // record the change
12831 + History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
12833 + if ($send_notification_to == 'all') {
12835 + $from = User::getFromHeader($usr_id);
12836 + $message_id = User::getFromHeader($usr_id);
12837 + $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
12838 + '', '', 'Issue closed comments', $reason, '');
12840 + $structure = Mime_Helper::decode($full_email, true, false);
12843 + 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
12844 + 'issue_id' => $issue_id,
12845 + 'message_id' => $message_id,
12846 + 'date' => Date_Helper::getCurrentDateGMT(),
12847 + 'subject' => 'Issue closed comments',
12849 + 'has_attachment'=> 0,
12850 + 'body' => $reason,
12851 + 'full_email' => $full_email,
12852 + 'headers' => $structure->headers
12854 + Support::insertEmail($email, $structure, $sup_id, true);
12857 + // add note with the reason to close the issue
12858 + $_POST['title'] = 'Issue closed comments';
12859 + $_POST["note"] = $reason;
12860 + Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
12864 + if ($send_notification) {
12865 + if (Customer::hasCustomerIntegration($prj_id)) {
12866 + // send a special confirmation email when customer issues are closed
12868 + iss_customer_contact_id
12870 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12872 + iss_id=$issue_id";
12873 + $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
12874 + if (!empty($customer_contact_id)) {
12875 + Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
12878 + // send notifications for the issue being closed
12879 + Notification::notify($issue_id, 'closed', $ids);
12881 + Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
12888 + * Method used to update the details of a specific issue.
12891 + * @param integer $issue_id The issue ID
12892 + * @return integer 1 if the update worked, -1 or -2 otherwise
12894 + function update($issue_id)
12897 + $errors = array();
12899 + $issue_id = Misc::escapeInteger($issue_id);
12901 + $usr_id = Auth::getUserID();
12902 + $prj_id = self::getProjectID($issue_id);
12904 + $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
12905 + if ($workflow !== true) {
12906 + return $workflow;
12909 + // get all of the 'current' information of this issue
12910 + $current = self::getDetails($issue_id);
12911 + // update the issue associations
12912 + if (empty($_POST['associated_issues'])) {
12913 + $associated_issues = array();
12915 + $associated_issues = explode(',', @$_POST['associated_issues']);
12916 + // make sure all associated issues are valid (and in this project)
12917 + for ($i = 0; $i < count($associated_issues); $i++) {
12918 + if (!self::exists(trim($associated_issues[$i]), false)) {
12919 + $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
12920 + unset($associated_issues[$i]);
12924 + $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
12925 + if (count($association_diff) > 0) {
12926 + // go through the new assocations, if association already exists, skip it
12927 + $associations_to_remove = $current['associated_issues'];
12928 + if (count($associated_issues) > 0) {
12929 + foreach ($associated_issues as $index => $associated_id) {
12930 + if (!in_array($associated_id, $current['associated_issues'])) {
12931 + self::addAssociation($issue_id, $associated_id, $usr_id);
12933 + // already assigned, remove this user from list of users to remove
12934 + unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
12938 + if (count($associations_to_remove) > 0) {
12939 + foreach ($associations_to_remove as $associated_id) {
12940 + self::deleteAssociation($issue_id, $associated_id);
12944 + $assignments_changed = false;
12945 + if (@$_POST["keep_assignments"] == "no") {
12946 + // only change the issue-user associations if there really were any changes
12947 + $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
12948 + if (!empty($_POST['assignments'])) {
12949 + $new_assignees = @$_POST['assignments'];
12951 + $new_assignees = array();
12953 + $assignment_notifications = array();
12955 + // remove people from the assignment list, if appropriate
12956 + foreach ($old_assignees as $assignee) {
12957 + if (!in_array($assignee, $new_assignees)) {
12958 + self::deleteUserAssociation($issue_id, $assignee);
12959 + $assignments_changed = true;
12962 + // add people to the assignment list, if appropriate
12963 + foreach ($new_assignees as $assignee) {
12964 + if (!in_array($assignee, $old_assignees)) {
12965 + self::addUserAssociation($usr_id, $issue_id, $assignee);
12966 + Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
12967 + $assignment_notifications[] = $assignee;
12968 + $assignments_changed = true;
12971 + if (count($assignment_notifications) > 0) {
12972 + Notification::notifyNewAssignment($assignment_notifications, $issue_id);
12975 + if (empty($_POST["estimated_dev_time"])) {
12976 + $_POST["estimated_dev_time"] = 0;
12979 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12981 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12982 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12983 + iss_last_public_action_type='updated',";
12984 + if (!empty($_POST["category"])) {
12985 + $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
12987 + if (@$_POST["keep"] == "no") {
12988 + $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
12990 + if (!empty($_POST['expected_resolution_date'])) {
12991 + $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
12993 + $stmt .= "iss_expected_resolution_date=null,";
12996 + iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
12997 + iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
12998 + iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
12999 + iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
13000 + iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
13001 + iss_description='" . Misc::escapeString($_POST["description"]) . "',
13002 + iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
13003 + iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
13004 + iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
13005 + iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
13006 + if (isset($_POST['private'])) {
13008 + iss_private = " . Misc::escapeInteger($_POST['private']);
13012 + iss_id=$issue_id";
13013 + $res = DB_Helper::getInstance()->query($stmt);
13014 + if (PEAR::isError($res)) {
13015 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13018 + // add change to the history (only for changes on specific fields?)
13019 + $updated_fields = array();
13020 + if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
13021 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
13023 + if ($current["iss_prc_id"] != $_POST["category"]) {
13024 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
13026 + if ($current["iss_pre_id"] != $_POST["release"]) {
13027 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
13029 + if ($current["iss_pri_id"] != $_POST["priority"]) {
13030 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
13031 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
13033 + if ($current["iss_sta_id"] != $_POST["status"]) {
13034 + // clear out the last-triggered-reminder flag when changing the status of an issue
13035 + Reminder_Action::clearLastTriggered($issue_id);
13037 + // if old status was closed and new status is not, clear closed data from issue.
13038 + $old_status_details = Status::getDetails($current['iss_sta_id']);
13039 + if ($old_status_details['sta_is_closed'] == 1) {
13040 + $new_status_details = Status::getDetails($_POST["status"]);
13041 + if ($new_status_details['sta_is_closed'] != 1) {
13042 + self::clearClosed($issue_id);
13045 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
13047 + if ($current["iss_res_id"] != $_POST["resolution"]) {
13048 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
13050 + if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
13051 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
13053 + if ($current["iss_summary"] != $_POST["summary"]) {
13054 + $updated_fields["Summary"] = '';
13056 + if ($current["iss_description"] != $_POST["description"]) {
13057 + $updated_fields["Description"] = '';
13059 + if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
13060 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
13062 + if (count($updated_fields) > 0) {
13063 + // log the changes
13066 + foreach ($updated_fields as $key => $value) {
13068 + $changes .= "; ";
13070 + if (($key != "Summary") && ($key != "Description")) {
13071 + $changes .= "$key: $value";
13073 + $changes .= "$key";
13077 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
13078 + // send notifications for the issue being updated
13079 + Notification::notifyIssueUpdated($issue_id, $current, $_POST);
13082 + // record group change as a seperate change
13083 + if ($current["iss_grp_id"] != (int)$_POST["group"]) {
13084 + History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
13085 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
13088 + // now update any duplicates, if any
13089 + $update_dupe = array(
13096 + // COMPAT: the following line requires PHP > 4.0.4
13097 + $intersect = array_intersect($update_dupe, array_keys($updated_fields));
13098 + if (($current["duplicates"] != '') && (count($intersect) > 0)) {
13099 + self::updateDuplicates($issue_id);
13102 + // if there is customer integration, mark last customer action
13103 + if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
13104 + self::recordLastCustomerAction($issue_id);
13107 + if ($assignments_changed) {
13108 + // XXX: we may want to also send the email notification for those "new" assignees
13109 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
13112 + Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
13113 + // Move issue to another project
13114 + if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
13115 + $new_prj_id = (int)@$_POST['new_prj'];
13116 + if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
13117 + if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
13118 + $res = self::moveIssue($issue_id, $new_prj_id);
13119 + if ($res == -1) {
13132 + * Method used to update the a single detail field of a specific issue.
13134 + * @param integer $issue_id
13135 + * @param string $field_name
13136 + * @param string $field_value
13137 + * @param string $field_type string or integer (for escape)
13138 + * @return integer 1 on success, -1 otherwise
13140 + function updateField($issue_id, $field_name, $filed_value) {
13142 + $issue_id = Misc::escapeInteger($issue_id);
13144 + $usr_id = Auth::getUserID();
13145 + $prj_id = self::getProjectID($issue_id);
13147 + // get all of the 'current' information of this issue
13148 + $current = self::getDetails($issue_id);
13151 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13153 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
13154 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
13155 + iss_last_public_action_type='updated'";
13157 + switch ($field_name) {
13159 + $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
13162 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
13164 + case 'expected_resolution_date':
13165 + if (is_null($filed_value)) {
13166 + $stmt .= ", iss_expected_resolution_date = null";
13168 + $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
13172 + $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
13175 + $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
13178 + $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
13180 + case 'resolution':
13181 + $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
13184 + $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
13186 + case 'description':
13187 + $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
13189 + case 'estimated_dev_time':
13190 + $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
13192 + case 'percent_complete':
13193 + $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
13195 + case 'trigger_reminders':
13196 + $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
13199 + $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
13202 + $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
13205 + Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
13212 + iss_id=$issue_id";
13214 + $res = DB_Helper::getInstance()->query($stmt);
13215 + if (PEAR::isError($res)) {
13216 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13220 + 'category' => $current['iss_prc_id'],
13221 + 'release' => $current['iss_pre_id'],
13222 + 'expected_resolution_date' => $current['iss_expected_resolution_date'],
13223 + 'release' => $current['iss_pre_id'],
13224 + 'priority' => $current['iss_pri_id'],
13225 + 'status' => $current['iss_sta_id'],
13226 + 'resolution' => $current['iss_res_id'],
13227 + 'summary' => $current['iss_summary'],
13228 + 'description' => $current['iss_description'],
13229 + 'estimated_dev_time' => $current['iss_dev_time'],
13230 + 'percent_complete' => $current['iss_percent_complete'],
13231 + 'trigger_reminders' => $current['iss_trigger_reminders'],
13232 + 'group' => $current['iss_grp_id'],
13233 + 'iss_private' => $current['private']
13235 + $new[$field_name] = $filed_value;
13237 + // add change to the history (only for changes on specific fields?)
13238 + $updated_fields = array();
13239 + if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
13240 + $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
13242 + if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
13243 + $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
13245 + if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
13246 + $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
13248 + if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
13249 + $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
13250 + Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
13252 + if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
13253 + // clear out the last-triggered-reminder flag when changing the status of an issue
13254 + Reminder_Action::clearLastTriggered($issue_id);
13256 + // if old status was closed and new status is not, clear closed data from issue.
13257 + $old_status_details = Status::getDetails($current['iss_sta_id']);
13258 + if ($old_status_details['sta_is_closed'] == 1) {
13259 + $new_status_details = Status::getDetails($filed_value);
13260 + if ($new_status_details['sta_is_closed'] != 1) {
13261 + self::clearClosed($issue_id);
13264 + $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
13266 + if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
13267 + $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
13269 + if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
13270 + $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
13272 + if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
13273 + $updated_fields["Summary"] = '';
13275 + if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
13276 + $updated_fields["Description"] = '';
13278 + if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
13279 + $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
13281 + if (count($updated_fields) > 0) {
13282 + // log the changes
13285 + foreach ($updated_fields as $key => $value) {
13287 + $changes .= "; ";
13289 + if (($key != "Summary") && ($key != "Description")) {
13290 + $changes .= "$key: $value";
13292 + $changes .= "$key";
13297 + History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
13298 + // send notifications for the issue being updated
13299 + Notification::notifyIssueUpdated($issue_id, $current, $new);
13307 + * Move the issue to a new project
13309 + * @param integer $issue_id
13310 + * @param integer $new_prj_id
13311 + * @return integer 1 on success, -1 otherwise
13313 + function moveIssue($issue_id, $new_prj_id)
13316 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13318 + iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
13320 + iss_id = " . Misc::escapeInteger($issue_id);
13321 + $res = DB_Helper::getInstance()->query($stmt);
13322 + if (PEAR::isError($res)) {
13323 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13326 + $currentDetails = self::getDetails($issue_id);
13328 + // set new category
13329 + $new_iss_prc_list = Category::getAssocList($new_prj_id);
13330 + $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
13331 + $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
13332 + if ($new_prc_id === false) {
13333 + // use the first category listed in the new project
13334 + $new_prc_id = key($new_iss_prc_list);
13337 + // set new priority
13338 + $new_iss_pri_list = Priority::getAssocList($new_prj_id);
13339 + $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
13340 + $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
13341 + if ($new_pri_id === false) {
13342 + // use the first category listed in the new project
13343 + $new_pri_id = key($new_iss_pri_list);
13346 + // XXX: Set status if needed when moving issue
13349 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13351 + iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
13352 + iss_pri_id=" . $new_pri_id . "
13354 + iss_id=$issue_id";
13355 + $res = DB_Helper::getInstance()->query($stmt);
13356 + if (PEAR::isError($res)) {
13357 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13360 + // clear project cache
13361 + self::getProjectID($issue_id, true);
13363 + Notification::notifyNewIssue($new_prj_id, $issue_id);
13369 + * Method used to associate an existing issue with another one.
13372 + * @param integer $issue_id The issue ID
13373 + * @param integer $issue_id The other issue ID
13376 + function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
13378 + $issue_id = Misc::escapeInteger($issue_id);
13379 + $associated_id = Misc::escapeInteger($associated_id);
13381 + $stmt = "INSERT INTO
13382 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13385 + isa_associated_id
13390 + DB_Helper::getInstance()->query($stmt);
13391 + History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
13392 + // link the associated issue back to this one
13393 + if ($link_issues) {
13394 + self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
13400 + * Method used to remove the issue associations related to a specific issue.
13403 + * @param integer $issue_id The issue ID
13406 + function deleteAssociations($issue_id, $usr_id = FALSE)
13408 + $issue_id = Misc::escapeInteger($issue_id);
13409 + if (is_array($issue_id)) {
13410 + $issue_id = implode(", ", $issue_id);
13412 + $stmt = "DELETE FROM
13413 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13415 + isa_issue_id IN ($issue_id) OR
13416 + isa_associated_id IN ($issue_id)";
13417 + DB_Helper::getInstance()->query($stmt);
13419 + History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
13425 + * Method used to remove a issue association from an issue.
13428 + * @param integer $issue_id The issue ID
13429 + * @param integer $associated_id The associated issue ID to remove.
13432 + function deleteAssociation($issue_id, $associated_id)
13434 + $issue_id = Misc::escapeInteger($issue_id);
13435 + $associated_id = Misc::escapeInteger($associated_id);
13436 + $stmt = "DELETE FROM
13437 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13440 + isa_issue_id = $issue_id AND
13441 + isa_associated_id = $associated_id
13444 + isa_issue_id = $associated_id AND
13445 + isa_associated_id = $issue_id
13447 + DB_Helper::getInstance()->query($stmt);
13448 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
13449 + "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
13450 + History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
13451 + "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
13456 + * Method used to assign an issue with an user.
13459 + * @param integer $usr_id The user ID of the person performing this change
13460 + * @param integer $issue_id The issue ID
13461 + * @param integer $assignee_usr_id The user ID of the assignee
13462 + * @param boolean $add_history Whether to add a history entry about this or not
13463 + * @return integer 1 if the update worked, -1 otherwise
13465 + function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
13467 + $issue_id = Misc::escapeInteger($issue_id);
13468 + $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
13470 + // move all orders down to free "order space" for this new association
13472 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13474 + isu_order = isu_order + 1
13476 + isu_usr_id = $assignee_usr_id AND
13477 + isu_order >= $order";
13478 + $res = DB_Helper::getInstance()->query($stmt);
13479 + if (PEAR::isError($res)) {
13480 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13483 + // insert the new association
13484 + $stmt = "INSERT INTO
13485 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13489 + isu_assigned_date,
13493 + $assignee_usr_id,
13494 + '" . Date_Helper::getCurrentDateGMT() . "',
13497 + $res = DB_Helper::getInstance()->query($stmt);
13498 + if (PEAR::isError($res)) {
13499 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13502 + if ($add_history) {
13503 + History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
13504 + 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
13511 + * Method used to get the order list to be rearranged
13513 + * @access private
13514 + * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
13515 + * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
13516 + * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
13518 + function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
13520 + // find all affected associantion orders
13521 + $stmt = "SELECT isu_usr_id, isu_order FROM
13522 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13524 + isu_iss_id IN ($issue_id)";
13525 + if ($usr_id !== FALSE) {
13526 + $stmt.= " AND isu_usr_id IN ($usr_id)";
13528 + $stmt.= "ORDER BY isu_order";
13529 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
13530 + if (PEAR::isError($res)) {
13531 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13534 + $deleted_orders = array();
13535 + foreach ($res as $row) {
13536 + if (empty($deleted_orders[$row['isu_usr_id']])) {
13537 + $deleted_orders[$row['isu_usr_id']] = array();
13539 + $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
13541 + return $deleted_orders;
13547 + * Method used to rearrange order list in the db according to known deleted records
13549 + * @access private
13550 + * @param mixed deleteorder list
13553 + function rearrangeDeleteUserAssociationOrderList($delete_order_list)
13555 + if (empty($delete_order_list) || (!is_array($delete_order_list))) {
13558 + foreach ($delete_order_list as $isu_usr_id => $orders) {
13559 + for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
13560 + // move the orders after them up to take the "order space" of the deleted records
13562 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13564 + isu_order = isu_order - " . ($i+1) . "
13566 + isu_usr_id = $isu_usr_id AND
13567 + isu_order > " . $orders[$i];
13568 + if ($i < count($orders) - 1) {
13570 + isu_order < " . $orders[$i+1];
13572 + $res = DB_Helper::getInstance()->query($stmt);
13573 + if (PEAR::isError($res)) {
13574 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13584 + * Method used to delete all user assignments for a specific issue.
13587 + * @param integer $issue_id The issue ID
13588 + * @param integer $usr_id The user ID of the person performing the change
13591 + function deleteUserAssociations($issue_id, $usr_id = FALSE)
13593 + $issue_id = Misc::escapeInteger($issue_id);
13594 + if (is_array($issue_id)) {
13595 + $issue_id = implode(", ", $issue_id);
13597 + $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
13598 + $stmt = "DELETE FROM
13599 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13601 + isu_iss_id IN ($issue_id)";
13602 + $res = DB_Helper::getInstance()->query($stmt);
13603 + if (PEAR::isError($res)) {
13604 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13608 + History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
13610 + self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
13617 + * Method used to delete a single user assignments for a specific issue.
13620 + * @param integer $issue_id The issue ID
13621 + * @param integer $usr_id The user to remove.
13622 + * @param boolean $add_history Whether to add a history entry about this or not
13625 + function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
13627 + $issue_id = Misc::escapeInteger($issue_id);
13628 + $usr_id = Misc::escapeInteger($usr_id);
13629 + $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
13630 + $stmt = "DELETE FROM
13631 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13633 + isu_iss_id = $issue_id AND
13634 + isu_usr_id = $usr_id";
13635 + $res = DB_Helper::getInstance()->query($stmt);
13636 + if (PEAR::isError($res)) {
13637 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13640 + if ($add_history) {
13641 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
13642 + User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
13644 + self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
13651 + * Creates an issue with the given email information.
13654 + * @param integer $prj_id The project ID
13655 + * @param integer $usr_id The user responsible for this action
13656 + * @param string $sender The original sender of this email
13657 + * @param string $summary The issue summary
13658 + * @param string $description The issue description
13659 + * @param integer $category The category ID
13660 + * @param integer $priority The priority ID
13661 + * @param array $assignment The list of users to assign this issue to
13662 + * @param string $date The date the email was originally sent.
13663 + * @param string $msg_id The message ID of the email we are creating this issue from.
13666 + function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
13669 + $exclude_list = array();
13671 + $sender_email = Mail_Helper::getEmailAddress($sender);
13672 + $sender_usr_id = User::getUserIDByEmail($sender_email, true);
13673 + if (!empty($sender_usr_id)) {
13674 + $reporter = $sender_usr_id;
13675 + $exclude_list[] = $sender_usr_id;
13679 + 'category' => $category,
13680 + 'priority' => $priority,
13681 + 'description' => $description,
13682 + 'summary' => $summary,
13683 + 'msg_id' => $msg_id,
13686 + if (Customer::hasCustomerIntegration($prj_id)) {
13687 + list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
13688 + if (!empty($customer_id)) {
13689 + $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
13690 + // overwrite the reporter with the customer contact
13691 + $reporter = User::getUserIDByContactID($customer_contact_id);
13692 + $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
13694 + $data['customer'] = $customer_id;
13695 + $data['contact'] = $customer_contact_id;
13696 +# $data['contract'] = // XXX missing
13697 + $data['contact_person_lname'] = $contact['last_name'];
13698 + $data['contact_person_fname'] = $contact['first_name'];
13699 + $data['contact_email'] = $sender_email;
13700 + $data['contact_phone'] = $contact['phone'];
13701 + $data['contact_timezone'] = $contact_timezone;
13704 + $customer_id = FALSE;
13706 + if (empty($reporter)) {
13707 + $reporter = APP_SYSTEM_USER_ID;
13710 + $data['reporter'] = $reporter;
13712 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
13713 + if ($issue_id == -1) {
13717 + $has_TAM = false;
13719 + // log the creation of the issue
13720 + History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
13722 + $emails = array();
13723 + $manager_usr_ids = array();
13724 + if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
13725 + // if there are any technical account managers associated with this customer, add these users to the notification list
13726 + $managers = Customer::getAccountManagers($prj_id, $customer_id);
13727 + $manager_usr_ids = array_keys($managers);
13728 + $manager_emails = array_values($managers);
13729 + $emails = array_merge($emails, $manager_emails);
13731 + // add the reporter to the notification list
13732 + $emails[] = $sender;
13733 + $emails = array_unique($emails);
13734 + $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
13735 + foreach ($emails as $address) {
13736 + Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
13739 + // only assign the issue to an user if the associated customer has any technical account managers
13740 + $users = array();
13741 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
13742 + foreach ($manager_usr_ids as $manager_usr_id) {
13743 + $users[] = $manager_usr_id;
13744 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
13745 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
13749 + // now add the user/issue association
13750 + if (@count($assignment) > 0) {
13751 + for ($i = 0; $i < count($assignment); $i++) {
13752 + Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
13753 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
13754 + if ($assignment[$i] != $usr_id) {
13755 + $users[] = $assignment[$i];
13759 + // only use the round-robin feature if this new issue was not
13760 + // already assigned to a customer account manager
13761 + if (@count($manager_usr_ids) < 1) {
13762 + $assignee = Round_Robin::getNextAssignee($prj_id);
13763 + // assign the issue to the round robin person
13764 + if (!empty($assignee)) {
13765 + self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
13766 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
13767 + $users[] = $assignee;
13772 + if (count($users) > 0) {
13773 + $has_assignee = true;
13776 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
13778 + // send special 'an issue was auto-created for you' notification back to the sender
13779 + Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
13781 + // also notify any users that want to receive emails anytime a new issue is created
13782 + Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
13784 + return $issue_id;
13789 + * Return errors that happened when creating new issue from POST method.
13793 + private static $insert_errors = array();
13794 + static function getInsertErrors() {
13795 + return self::$insert_errors;
13799 + * Method used to add a new issue using the normal report form.
13802 + * @return integer The new issue ID
13804 + function createFromPost()
13807 + 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
13808 + 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
13809 + 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
13812 + foreach ($keys as $key) {
13813 + if (isset($_POST[$key])) {
13814 + $data[$key] = $_POST[$key];
13818 + $prj_id = Auth::getCurrentProject();
13819 + $usr_id = Auth::getUserID();
13821 + // if we are creating an issue for a customer, put the
13822 + // main customer contact as the reporter for it
13823 + if (Customer::hasCustomerIntegration($prj_id)) {
13824 + $contact_usr_id = User::getUserIDByContactID($data['contact']);
13825 + if (empty($contact_usr_id)) {
13826 + $contact_usr_id = $usr_id;
13828 + $data['reporter'] = $contact_usr_id;
13830 + $data['reporter'] = $usr_id;
13833 + $data['msg_id'] = Mail_Helper::generateMessageID();
13835 + $issue_id = self::insertIssue($prj_id, $usr_id, $data);
13836 + if ($issue_id == -1) {
13840 + $has_TAM = false;
13842 + $info = User::getNameEmail($usr_id);
13843 + // log the creation of the issue
13844 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
13846 + $emails = array();
13847 + if (Customer::hasCustomerIntegration($prj_id)) {
13848 + if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
13849 + $emails = $data['contact_extra_emails'];
13851 + // add the primary contact to the notification list
13852 + if ($data['add_primary_contact'] == 'yes') {
13853 + $contact_email = User::getEmailByContactID($data['contact']);
13854 + if (!empty($contact_email)) {
13855 + $emails[] = $contact_email;
13858 + // if there are any technical account managers associated with this customer, add these users to the notification list
13859 + $managers = Customer::getAccountManagers($prj_id, $data['customer']);
13860 + $manager_usr_ids = array_keys($managers);
13861 + $manager_emails = array_values($managers);
13862 + $emails = array_merge($emails, $manager_emails);
13864 + // add the reporter to the notification list
13865 + $emails[] = $info['usr_email'];
13866 + $emails = array_unique($emails);
13867 + foreach ($emails as $address) {
13868 + Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
13871 + // only assign the issue to an user if the associated customer has any technical account managers
13872 + $users = array();
13873 + $has_TAM = false;
13874 + if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
13875 + foreach ($manager_usr_ids as $manager_usr_id) {
13876 + $users[] = $manager_usr_id;
13877 + self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
13878 + History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
13882 + // now add the user/issue association (aka assignments)
13883 + if (!empty($data['users']) && count($data['users']) > 0) {
13884 + for ($i = 0; $i < count($data['users']); $i++) {
13885 + Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
13886 + Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
13887 + self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
13888 + if ($data['users'][$i] != $usr_id) {
13889 + $users[] = $data['users'][$i];
13893 + // only use the round-robin feature if this new issue was not
13894 + // already assigned to a customer account manager
13895 + if (@count($manager_usr_ids) < 1) {
13896 + $assignee = Round_Robin::getNextAssignee($prj_id);
13897 + // assign the issue to the round robin person
13898 + if (!empty($assignee)) {
13899 + $users[] = $assignee;
13900 + self::addUserAssociation($usr_id, $issue_id, $assignee, false);
13901 + History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
13907 + // now process any files being uploaded
13909 + for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
13910 + if (!@empty($_FILES["file"]["name"][$i])) {
13916 + $files = array();
13917 + for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
13918 + $filename = @$_FILES["file"]["name"][$i];
13919 + if (empty($filename)) {
13922 + $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
13923 + if (empty($blob)) {
13924 + // error reading a file
13925 + self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
13928 + $files[] = array(
13929 + "filename" => $filename,
13930 + "type" => $_FILES['file']['type'][$i],
13934 + if (count($files) > 0) {
13935 + $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
13936 + foreach ($files as $file) {
13937 + Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
13941 + // need to associate any emails ?
13942 + if (!empty($data['attached_emails'])) {
13943 + $items = explode(",", $data['attached_emails']);
13944 + Support::associate($usr_id, $issue_id, $items);
13946 + // need to notify any emails being converted into issues ?
13947 + if (@count($data['notify_senders']) > 0) {
13948 + $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
13950 + $recipients = array();
13952 + // need to process any custom fields ?
13953 + if (@count($data['custom_fields']) > 0) {
13954 + foreach ($data['custom_fields'] as $fld_id => $value) {
13955 + Custom_Field::associateIssue($issue_id, $fld_id, $value);
13958 + // also send a special confirmation email to the customer contact
13959 + if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
13960 + // also need to pass the list of sender emails already notified,
13961 + // so we can avoid notifying the same person again
13962 + $contact_email = User::getEmailByContactID($data['contact']);
13963 + if (@!in_array($contact_email, $recipients)) {
13964 + Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
13968 + Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
13970 + // also notify any users that want to receive emails anytime a new issue is created
13971 + Notification::notifyNewIssue($prj_id, $issue_id);
13973 + return $issue_id;
13977 + * Insert issue to database.
13979 + * @param integer $prj_id The project ID
13980 + * @param integer $usr_id The user responsible for this action
13981 + * @param array $data of issue to be inserted
13982 + * @return integer The new issue ID
13984 + private function insertIssue($prj_id, $usr_id, $data)
13987 + // XXX missing_fields never used
13988 + $missing_fields = array();
13989 + if ($data['category'] == -1) {
13990 + $missing_fields[] = 'Category';
13992 + if ($data['priority'] == -1) {
13993 + $missing_fields[] = 'Priority';
13996 + // if there is no reporter set, use the system user
13997 + if (empty($data['reporter'])) {
13998 + $data['reporter'] = APP_SYSTEM_USER_ID;
14001 + if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
14002 + $data['estimated_dev_time'] = 0;
14006 + $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
14008 + "iss_prj_id=" . $prj_id . ",";
14009 + if (!empty($data['group'])) {
14010 + $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
14012 + if (!empty($data['category'])) {
14013 + $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
14015 + if (!empty($data['release'])) {
14016 + $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
14018 + if (!empty($data['priority'])) {
14019 + $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
14022 + $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
14024 + $initial_status = Project::getInitialStatus($prj_id);
14025 + if (!empty($initial_status)) {
14026 + $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
14029 + if (Customer::hasCustomerIntegration($prj_id)) {
14031 + iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
14032 + if (!empty($data['contact'])) {
14034 + iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
14037 + iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
14038 + iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
14039 + iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
14040 + iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
14041 + iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
14042 + iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
14046 + iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
14047 + iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
14048 + iss_last_public_action_type='created',
14049 + iss_summary='" . Misc::escapeString($data['summary']) . "',
14050 + iss_description='" . Misc::escapeString($data['description']) . "',
14051 + iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
14052 + if (!empty($data['contact'])) {
14054 + iss_private=" . Misc::escapeInteger($data['private']) . " ,";
14057 + iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
14060 + $res = DB_Helper::getInstance()->query($stmt);
14061 + if (PEAR::isError($res)) {
14062 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14066 + $issue_id = DB_Helper::get_last_insert_id();
14067 + return $issue_id;
14072 + * Method used to get a specific parameter in the issue listing cookie.
14075 + * @param string $name The name of the parameter
14076 + * @return mixed The value of the specified parameter
14078 + function getParam($name)
14080 + $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
14082 + if (isset($_GET[$name])) {
14083 + return $_GET[$name];
14084 + } elseif (isset($_POST[$name])) {
14085 + return $_POST[$name];
14086 + } elseif (isset($profile[$name])) {
14087 + return $profile[$name];
14095 + * Method used to save the current search parameters in a cookie.
14098 + * @return array The search parameters
14100 + function saveSearchParams()
14102 + $sort_by = self::getParam('sort_by');
14103 + $sort_order = self::getParam('sort_order');
14104 + $users = self::getParam('users');
14105 + if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
14107 + unset($sort_order);
14109 + $rows = self::getParam('rows');
14110 + $hide_closed = self::getParam('hide_closed');
14111 + if ($hide_closed === '') {
14112 + $hide_closed = 1;
14114 + $search_type = self::getParam('search_type');
14115 + if (empty($search_type)) {
14116 + $search_type = 'all_text';
14118 + $custom_field = self::getParam('custom_field');
14119 + if (is_string($custom_field)) {
14120 + $custom_field = unserialize(urldecode($custom_field));
14123 + 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
14124 + 'pagerRow' => self::getParam('pagerRow'),
14125 + 'hide_closed' => $hide_closed,
14126 + "sort_by" => $sort_by ? $sort_by : "pri_rank",
14127 + "sort_order" => $sort_order ? $sort_order : "ASC",
14128 + // quick filter form
14129 + 'keywords' => self::getParam('keywords'),
14130 + 'search_type' => $search_type,
14131 + 'users' => self::getParam('users'),
14132 + 'status' => self::getParam('status'),
14133 + 'priority' => self::getParam('priority'),
14134 + 'category' => self::getParam('category'),
14135 + 'customer_email' => self::getParam('customer_email'),
14136 + // advanced search form
14137 + 'show_authorized_issues' => self::getParam('show_authorized_issues'),
14138 + 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
14139 + 'reporter' => self::getParam('reporter'),
14141 + 'release' => self::getParam('release'),
14143 + 'custom_field' => $custom_field
14145 + // now do some magic to properly format the date fields
14146 + $date_fields = array(
14149 + 'last_response_date',
14150 + 'first_response_date',
14153 + foreach ($date_fields as $field_name) {
14154 + $field = self::getParam($field_name);
14155 + if (empty($field)) {
14158 + if (@$field['filter_type'] == 'in_past') {
14159 + @$cookie[$field_name] = array(
14160 + 'filter_type' => 'in_past',
14161 + 'time_period' => $field['time_period']
14164 + $end_field_name = $field_name . '_end';
14165 + $end_field = self::getParam($end_field_name);
14166 + @$cookie[$field_name] = array(
14167 + 'past_hour' => $field['past_hour'],
14168 + 'Year' => $field['Year'],
14169 + 'Month' => $field['Month'],
14170 + 'Day' => $field['Day'],
14171 + 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
14172 + 'filter_type' => $field['filter_type'],
14173 + 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
14175 + @$cookie[$end_field_name] = array(
14176 + 'Year' => $end_field['Year'],
14177 + 'Month' => $end_field['Month'],
14178 + 'Day' => $end_field['Day']
14182 + Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
14188 + * Method used to get the current sorting options used in the grid layout
14189 + * of the issue listing page.
14192 + * @param array $options The current search parameters
14193 + * @return array The sorting options
14195 + function getSortingInfo($options)
14198 + $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
14200 + // default order for last action date, priority should be descending
14201 + // for textual fields, like summary, ascending is reasonable
14203 + "pri_rank" => "desc",
14204 + "iss_id" => "desc",
14205 + "iss_customer_id" => "desc",
14206 + "prc_title" => "asc",
14207 + "sta_rank" => "asc",
14208 + "iss_created_date" => "desc",
14209 + "iss_summary" => "asc",
14210 + "last_action_date" => "desc",
14211 + "usr_full_name" => "asc",
14212 + "iss_expected_resolution_date" => "desc",
14213 + "pre_title" => "asc",
14214 + "assigned" => "asc",
14215 + "isu_order" => "desc",
14218 + foreach ($custom_fields as $fld_id => $fld_name) {
14219 + $fields['custom_field_' . $fld_id] = "desc";
14222 + $sortfields = array_combine(array_keys($fields), array_keys($fields));
14223 + $sortfields["pre_title"] = "pre_scheduled_date";
14224 + $sortfields["assigned"] = "isu_usr_id";
14227 + "links" => array(),
14228 + "images" => array()
14230 + foreach ($sortfields as $field => $sortfield) {
14231 + $sort_order = $fields[$field];
14232 + if ($options["sort_by"] == $sortfield) {
14233 + $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
14234 + if (strtolower($options["sort_order"]) == "asc") {
14235 + $sort_order = "desc";
14237 + $sort_order = "asc";
14240 + $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
14247 + * Returns the list of action date fields appropriate for the
14248 + * current user ID.
14251 + * @return array The list of action date fields
14253 + function getLastActionFields()
14255 + $last_action_fields = array(
14256 + "iss_last_public_action_date"
14258 + if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
14259 + $last_action_fields[] = "iss_last_internal_action_date";
14261 + if (count($last_action_fields) > 1) {
14262 + return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
14264 + return $last_action_fields[0] . " AS last_action_date";
14270 + * Method used to get the list of issues to be displayed in the grid layout.
14273 + * @param integer $prj_id The current project ID
14274 + * @param array $options The search parameters
14275 + * @param integer $current_row The current page number
14276 + * @param integer $max The maximum number of rows per page
14277 + * @return array The list of issues to be displayed
14279 + function getListing($prj_id, $options, $current_row = 0, $max = 5)
14281 + if (strtoupper($max) == "ALL") {
14284 + $start = $current_row * $max;
14285 + // get the current user's role
14286 + $usr_id = Auth::getUserID();
14287 + $role_id = User::getRoleByUser($usr_id, $prj_id);
14289 + // get any custom fields that should be displayed
14290 + $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
14298 + iss_customer_contract_id,
14299 + iss_created_date,
14300 + iss_updated_date,
14301 + iss_last_response_date,
14303 + iss_last_customer_action_date,
14309 + sta_color status_color,
14312 + grp_name `group`,
14314 + iss_last_public_action_date,
14315 + iss_last_public_action_type,
14316 + iss_last_internal_action_date,
14317 + iss_last_internal_action_type,
14318 + " . self::getLastActionFields() . ",
14319 + IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
14322 + iss_percent_complete,
14324 + iss_expected_resolution_date
14327 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14328 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
14329 + // join custom fields if we are searching by custom fields
14330 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14331 + foreach ($options['custom_field'] as $fld_id => $search_value) {
14332 + if (empty($search_value)) {
14335 + $field = Custom_Field::getDetails($fld_id);
14336 + if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14339 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14342 + if ($field['fld_type'] == 'multiple') {
14343 + $search_value = Misc::escapeInteger($search_value);
14344 + foreach ($search_value as $cfo_id) {
14345 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
14348 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
14353 + // check for the custom fields we want to sort by
14354 + if (strstr($options['sort_by'], 'custom_field') !== false) {
14355 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
14356 + $stmt .= "\n LEFT JOIN \n" .
14357 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
14359 + (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
14361 + if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
14364 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
14366 + isu_iss_id=iss_id";
14368 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
14371 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
14373 + iur_iss_id=iss_id";
14375 + if (!empty($options["show_notification_list_issues"])) {
14378 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
14380 + sub_iss_id=iss_id";
14384 + " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
14386 + iss_grp_id=grp_id
14388 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
14390 + iss_prc_id=prc_id
14392 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
14394 + iss_pre_id = pre_id
14396 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
14398 + iss_sta_id=sta_id
14400 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
14402 + iss_pri_id=pri_id
14404 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
14406 + iss_id=iqu_iss_id AND
14407 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
14409 + iss_prj_id= " . Misc::escapeInteger($prj_id);
14410 + $stmt .= self::buildWhereClause($options);
14412 + if (strstr($options["sort_by"], 'custom_field') !== false) {
14413 + $fld_details = Custom_Field::getDetails($fld_id);
14414 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
14416 + $sort_by = Misc::escapeString($options["sort_by"]);
14423 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
14425 + $total_rows = Pager::getTotalRows($stmt);
14428 + " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
14429 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
14430 + if (PEAR::isError($res)) {
14431 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14437 + if (count($res) > 0) {
14438 + self::getAssignedUsersByIssues($res);
14439 + Time_Tracking::getTimeSpentByIssues($res);
14440 + // need to get the customer titles for all of these issues...
14441 + if (Customer::hasCustomerIntegration($prj_id)) {
14442 + Customer::getCustomerTitlesByIssues($prj_id, $res);
14443 + Customer::getSupportLevelsByIssues($prj_id, $res);
14445 + self::formatLastActionDates($res);
14446 + self::getLastStatusChangeDates($prj_id, $res);
14447 + } elseif ($current_row > 0) {
14448 + // if there are no results, and the page is not the first page reset page to one and reload results
14449 + Auth::redirect("list.php?pagerRow=0&rows=$max");
14451 + $groups = Group::getAssocList($prj_id);
14452 + $categories = Category::getAssocList($prj_id);
14453 + $column_headings = self::getColumnHeadings($prj_id);
14454 + if (count($custom_fields) > 0) {
14455 + $column_headings = array_merge($column_headings,$custom_fields);
14457 + $csv[] = @implode("\t", $column_headings);
14458 + for ($i = 0; $i < count($res); $i++) {
14459 + $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
14460 + $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
14461 + $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
14463 + $res[$i]['pri_title'],
14464 + $res[$i]['iss_id'],
14465 + $res[$i]['usr_full_name'],
14467 + // hide the group column from the output if no
14468 + // groups are available in the database
14469 + if (count($groups) > 0) {
14470 + $fields[] = $res[$i]['group'];
14472 + $fields[] = $res[$i]['assigned_users'];
14473 + $fields[] = $res[$i]['time_spent'];
14474 + // hide the category column from the output if no
14475 + // categories are available in the database
14476 + if (count($categories) > 0) {
14477 + $fields[] = $res[$i]['prc_title'];
14479 + if (Customer::hasCustomerIntegration($prj_id)) {
14480 + $fields[] = @$res[$i]['customer_title'];
14481 + // check if current user is acustomer and has a per incident contract.
14482 + // if so, check if issue is redeemed.
14483 + if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
14484 + if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
14485 + (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
14486 + $res[$i]['redeemed'] = true;
14490 + $fields[] = $res[$i]['sta_title'];
14491 + $fields[] = $res[$i]["status_change_date"];
14492 + $fields[] = $res[$i]["last_action_date"];
14493 + $fields[] = $res[$i]['iss_dev_time'];
14494 + $fields[] = $res[$i]['iss_summary'];
14495 + $fields[] = $res[$i]['iss_expected_resolution_date'];
14497 + if (count($custom_fields) > 0) {
14498 + $res[$i]['custom_field'] = array();
14499 + $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
14500 + foreach ($custom_field_values as $this_field) {
14501 + if (!empty($custom_fields[$this_field['fld_id']])) {
14502 + $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
14503 + $fields[] = $this_field['value'];
14508 + $csv[] = @implode("\t", $fields);
14510 + $total_pages = ceil($total_rows / $max);
14511 + $last_page = $total_pages - 1;
14515 + "current_page" => $current_row,
14516 + "start_offset" => $start,
14517 + "end_offset" => $start + count($res),
14518 + "total_rows" => $total_rows,
14519 + "total_pages" => $total_pages,
14520 + "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
14521 + "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
14522 + "last_page" => $last_page,
14523 + "custom_fields" => $custom_fields
14525 + "csv" => @implode("\n", $csv)
14532 + * Processes a result set to format the "Last Action Date" column.
14535 + * @param array $result The result set
14537 + function formatLastActionDates(&$result)
14539 + for ($i = 0; $i < count($result); $i++) {
14540 + if (($result[$i]['action_type'] == "internal") &&
14541 + (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
14542 + $label = $result[$i]["iss_last_internal_action_type"];
14543 + $last_date = $result[$i]["iss_last_internal_action_date"];
14545 + $label = $result[$i]["iss_last_public_action_type"];
14546 + $last_date = $result[$i]["iss_last_public_action_date"];
14548 + $date = new Date($last_date);
14549 + $current = new Date(Date_Helper::getCurrentDateGMT());
14550 + $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
14551 + Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
14557 + * Retrieves the last status change date for the given issue.
14560 + * @param integer $prj_id The project ID
14561 + * @param array $result The associative array of data
14562 + * @see self::getListing()
14564 + function getLastStatusChangeDates($prj_id, &$result)
14567 + for ($i = 0; $i < count($result); $i++) {
14568 + $ids[] = $result[$i]["iss_sta_id"];
14570 + if (count($ids) == 0) {
14573 + $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
14574 + for ($i = 0; $i < count($result); $i++) {
14575 + if (empty($result[$i]['iss_sta_id'])) {
14576 + $result[$i]['status_change_date'] = '';
14578 + list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
14579 + if ((empty($label)) || (empty($date_field_name))) {
14580 + $result[$i]['status_change_date'] = '';
14583 + $current = new Date(Date_Helper::getCurrentDateGMT());
14584 + $desc = "$label: %s ago";
14585 + $target_date = $result[$i][$date_field_name];
14586 + if (empty($target_date)) {
14587 + $result[$i]['status_change_date'] = '';
14590 + $date = new Date($target_date);
14591 + $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
14598 + * Method used to get the list of issues to be displayed in the grid layout.
14601 + * @param array $options The search parameters
14602 + * @return string The where clause
14604 + function buildWhereClause($options)
14606 + $usr_id = Auth::getUserID();
14607 + $prj_id = Auth::getCurrentProject();
14608 + $role_id = User::getRoleByUser($usr_id, $prj_id);
14610 + $stmt = ' AND iss_usr_id = usr_id';
14611 + if ($role_id == User::getRoleID('Customer')) {
14612 + $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
14613 + } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
14615 + iss_usr_id = $usr_id OR
14616 + iur_usr_id = $usr_id
14620 + if (!empty($options["users"])) {
14621 + $stmt .= " AND (\n";
14622 + if (stristr($options["users"], "grp") !== false) {
14623 + $chunks = explode(":", $options["users"]);
14624 + $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
14626 + if ($options['users'] == '-1') {
14627 + $stmt .= 'isu_usr_id IS NULL';
14628 + } elseif ($options['users'] == '-2') {
14629 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
14630 + } elseif ($options['users'] == '-3') {
14631 + $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
14632 + } elseif ($options['users'] == '-4') {
14633 + $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
14635 + $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
14640 + if (!empty($options["reporter"])) {
14641 + $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
14643 + if (!empty($options["show_authorized_issues"])) {
14644 + $stmt .= " AND (iur_usr_id=$usr_id)";
14646 + if (!empty($options["show_notification_list_issues"])) {
14647 + $stmt .= " AND (sub_usr_id=$usr_id)";
14649 + if (!empty($options["keywords"])) {
14650 + $stmt .= " AND (\n";
14651 + if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
14652 + $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
14653 + } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
14654 + // check if the user is trying to search by customer email
14655 + $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
14656 + if (count($customer_ids) > 0) {
14657 + $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
14659 + // no results, kill query
14660 + $stmt .= " iss_customer_id = -1";
14663 + $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
14664 + $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
14668 + if (!empty($options["priority"])) {
14669 + $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
14671 + if (!empty($options["status"])) {
14672 + $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
14674 + if (!empty($options["category"])) {
14675 + if (!is_array($options['category'])) {
14676 + $options['category'] = array($options['category']);
14678 + $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
14680 + if (!empty($options["hide_closed"])) {
14681 + $stmt .= " AND sta_is_closed=0";
14683 + if (!empty($options['release'])) {
14684 + $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
14686 + // now for the date fields
14687 + $date_fields = array(
14690 + 'last_response_date',
14691 + 'first_response_date',
14694 + foreach ($date_fields as $field_name) {
14695 + if (!empty($options[$field_name])) {
14696 + switch ($options[$field_name]['filter_type']) {
14698 + $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
14701 + $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
14704 + $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
14707 + $stmt .= " AND iss_$field_name IS NULL";
14710 + if (strlen($options[$field_name]['time_period']) == 0) {
14711 + $options[$field_name]['time_period'] = 0;
14713 + $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
14714 + Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
14720 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14721 + foreach ($options['custom_field'] as $fld_id => $search_value) {
14722 + if (empty($search_value)) {
14725 + $field = Custom_Field::getDetails($fld_id);
14726 + $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
14727 + if (($field['fld_type'] == 'date') &&
14728 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14731 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14735 + if ($field['fld_type'] == 'multiple') {
14736 + $search_value = Misc::escapeInteger($search_value);
14737 + foreach ($search_value as $cfo_id) {
14738 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
14739 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
14740 + $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
14742 + } elseif ($field['fld_type'] == 'date') {
14743 + if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
14746 + $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
14747 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
14748 + cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
14749 + } else if ($field['fld_type'] == 'integer') {
14750 + $value = $search_value['value'];
14751 + switch ($search_value['filter_type']) {
14752 + case 'ge': $cmp = '>='; break;
14753 + case 'le': $cmp = '<='; break;
14754 + case 'gt': $cmp = '>'; break;
14755 + case 'lt': $cmp = '<'; break;
14756 + default: $cmp = '='; break;
14758 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
14759 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
14760 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
14762 + $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
14763 + $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
14764 + if ($field['fld_type'] == 'combo') {
14765 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
14767 + $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
14773 + // clear cached full-text values if we are not searching fulltext anymore
14774 + if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
14775 + Session::set('fulltext_string', '');
14776 + Session::set('fulltext_issues', '');
14783 + * Method used to get the previous and next issues that are available
14784 + * according to the current search parameters.
14787 + * @param integer $issue_id The issue ID
14788 + * @param array $options The search parameters
14789 + * @return array The list of issues
14791 + function getSides($issue_id, $options)
14793 + $usr_id = Auth::getUserID();
14794 + $role_id = Auth::getCurrentRole();
14798 + " . self::getLastActionFields() . "
14801 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14802 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
14803 + // join custom fields if we are searching by custom fields
14804 + if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14805 + foreach ($options['custom_field'] as $fld_id => $search_value) {
14806 + if (empty($search_value)) {
14809 + $field = Custom_Field::getDetails($fld_id);
14810 + if (($field['fld_type'] == 'date') &&
14811 + ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14814 + if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14818 + if ($field['fld_type'] == 'multiple') {
14819 + $search_value = Misc::escapeInteger($search_value);
14820 + foreach ($search_value as $cfo_id) {
14821 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
14824 + $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
14829 + // check for the custom fields we want to sort by
14830 + if (strstr($options['sort_by'], 'custom_field') !== false) {
14831 + $fld_id = str_replace("custom_field_", '', $options['sort_by']);
14832 + $stmt .= "\n LEFT JOIN \n" .
14833 + APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
14835 + (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
14837 + if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
14840 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
14842 + isu_iss_id=iss_id";
14844 + if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
14847 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
14849 + iur_iss_id=iss_id";
14851 + if (!empty($options["show_notification_list_issues"])) {
14854 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
14856 + sub_iss_id=iss_id";
14858 + if (@$options["sort_by"] == "pre_scheduled_date") {
14861 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
14863 + iss_pre_id = pre_id";
14865 + if (@$options['sort_by'] == 'prc_title') {
14868 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
14870 + iss_prc_id = prc_id";
14874 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
14876 + iss_sta_id=sta_id
14878 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
14880 + iss_pri_id=pri_id
14882 + iss_prj_id=" . Auth::getCurrentProject();
14883 + $stmt .= self::buildWhereClause($options);
14884 + if (strstr($options["sort_by"], 'custom_field') !== false) {
14885 + $fld_details = Custom_Field::getDetails($fld_id);
14886 + $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
14888 + $sort_by = Misc::escapeString($options["sort_by"]);
14894 + " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
14896 + $res = DB_Helper::getInstance()->getCol($stmt);
14897 + if (PEAR::isError($res)) {
14898 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14901 + // COMPAT: the next line requires PHP >= 4.0.5
14902 + $index = array_search($issue_id, $res);
14903 + if (!empty($res[$index+1])) {
14904 + $next = $res[$index+1];
14906 + if (!empty($res[$index-1])) {
14907 + $previous = $res[$index-1];
14910 + "next" => @$next,
14911 + "previous" => @$previous
14918 + * Method used to get the full list of user IDs assigned to a specific
14922 + * @param integer $issue_id The issue ID
14923 + * @return array The list of user IDs
14925 + function getAssignedUserIDs($issue_id)
14930 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
14931 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
14933 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
14934 + isu_usr_id=usr_id";
14935 + $res = DB_Helper::getInstance()->getCol($stmt);
14936 + if (PEAR::isError($res)) {
14937 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14946 + * Method used to see if a user is assigned to an issue.
14949 + * @param integer $issue_id The issue ID
14950 + * @param integer $usr_id An integer containg the ID of the user.
14951 + * @return boolean true if the user(s) are assigned to the issue.
14953 + function isAssignedToUser($issue_id, $usr_id)
14955 + $assigned_users = self::getAssignedUserIDs($issue_id);
14956 + if (in_array($usr_id, $assigned_users)) {
14965 + * Method used to get the full list of reporters associated with a given
14966 + * list of issues.
14969 + * @param array $result The result set
14972 + function getReportersByIssues(&$result)
14975 + for ($i = 0; $i < count($result); $i++) {
14976 + $ids[] = $result[$i]["iss_id"];
14978 + $ids = implode(", ", $ids);
14981 + CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
14983 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14984 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
14986 + iss_usr_id=usr_id AND
14987 + iss_id IN ($ids)";
14988 + $res = DB_Helper::getInstance()->getAssoc($stmt);
14989 + if (PEAR::isError($res)) {
14990 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14992 + // now populate the $result variable again
14993 + for ($i = 0; $i < count($result); $i++) {
14994 + @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
15001 + * Method used to get the full list of assigned users by a list
15002 + * of issues. This was originally created to optimize the issue
15006 + * @param array $result The result set
15009 + function getAssignedUsersByIssues(&$result)
15012 + for ($i = 0; $i < count($result); $i++) {
15013 + $ids[] = $result[$i]["iss_id"];
15015 + if (count($ids) < 1) {
15018 + $ids = implode(", ", $ids);
15025 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15026 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15028 + isu_usr_id=usr_id AND
15029 + isu_iss_id IN ($ids)";
15030 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15031 + if (PEAR::isError($res)) {
15032 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15034 + // gather names of the users assigned to each issue
15036 + for ($i = 0; $i < count($res); $i++) {
15037 + if (!empty($t[$res[$i]['isu_iss_id']])) {
15038 + $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
15040 + $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
15045 + for ($i = 0; $i < count($res); $i++) {
15046 + if (empty($o[$res[$i]['isu_iss_id']])) {
15047 + $o[$res[$i]['isu_iss_id']] = array();
15049 + $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
15051 + // now populate the $result variable again
15052 + for ($i = 0; $i < count($result); $i++) {
15053 + @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
15054 + @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
15061 + * Method used to add the issue description to a list of issues.
15064 + * @param array $result The result set
15067 + function getDescriptionByIssues(&$result)
15069 + if (count($result) == 0) {
15074 + for ($i = 0; $i < count($result); $i++) {
15075 + $ids[] = $result[$i]["iss_id"];
15077 + $ids = implode(", ", $ids);
15083 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15085 + iss_id in ($ids)";
15086 + $res = DB_Helper::getInstance()->getAssoc($stmt);
15087 + if (PEAR::isError($res)) {
15088 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15090 + for ($i = 0; $i < count($result); $i++) {
15091 + @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
15098 + * Method used to get the full list of users (the full names) assigned to a
15099 + * specific issue.
15102 + * @param integer $issue_id The issue ID
15103 + * @return array The list of users
15105 + function getAssignedUsers($issue_id)
15110 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15111 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15113 + isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
15114 + isu_usr_id=usr_id";
15115 + $res = DB_Helper::getInstance()->getCol($stmt);
15116 + if (PEAR::isError($res)) {
15117 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15126 + * Method used to get the details for a specific issue.
15129 + * @param integer $issue_id The issue ID
15130 + * @param boolean $force_refresh If the cache should not be used.
15131 + * @return array The details for the specified issue
15133 + function getDetails($issue_id, $force_refresh = false)
15137 + $issue_id = Misc::escapeInteger($issue_id);
15139 + if (empty($issue_id)) {
15143 + if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
15144 + return $returns[$issue_id];
15148 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
15154 + sta_abbreviation,
15155 + sta_color status_color,
15159 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15160 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
15163 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
15165 + iss_pri_id=pri_id
15167 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15169 + iss_sta_id=sta_id
15171 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
15173 + iss_prc_id=prc_id
15175 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
15177 + iss_pre_id=pre_id
15179 + iss_id=$issue_id AND
15180 + iss_prj_id=prj_id";
15181 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15182 + if (PEAR::isError($res)) {
15183 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15186 + if (empty($res)) {
15189 + $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
15190 + // get customer information, if any
15191 + if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
15192 + $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
15193 + $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
15194 + $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
15195 + $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
15196 + $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
15197 + $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
15198 + if (empty($res['iss_first_response_date'])) {
15199 + $first_response_deadline = $created_date_ts + $max_first_response_time;
15200 + if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
15201 + $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
15203 + $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
15207 + $res['iss_original_description'] = $res["iss_description"];
15208 + if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
15209 + $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
15210 + $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
15212 + $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
15213 + $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
15214 + $res['iss_created_date_ts'] = $created_date_ts;
15215 + $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
15216 + list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
15217 + $temp = self::getAssignedUsersStatus($res["iss_id"]);
15218 + $res["has_inactive_users"] = 0;
15219 + $res["assigned_users"] = array();
15220 + $res["assigned_inactive_users"] = array();
15221 + foreach ($temp as $usr_id => $usr_status) {
15222 + if (!User::isActiveStatus($usr_status)) {
15223 + $res["assigned_inactive_users"][] = $usr_id;
15224 + $res["has_inactive_users"] = 1;
15226 + $res["assigned_users"][] = $usr_id;
15229 + if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
15230 + $res["is_current_user_assigned"] = 1;
15232 + $res["is_current_user_assigned"] = 0;
15234 + $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
15235 + $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
15236 + $res["reporter"] = User::getFullName($res["iss_usr_id"]);
15237 + if (empty($res["iss_updated_date"])) {
15238 + $res["iss_updated_date"] = 'not updated yet';
15240 + $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
15242 + $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
15243 + if (Release::isAssignable($res["iss_pre_id"])) {
15244 + $release = Release::getDetails($res["iss_pre_id"]);
15245 + $res["pre_title"] = $release["pre_title"];
15246 + $res["pre_status"] = $release["pre_status"];
15248 + // need to return the list of issues that are duplicates of this one
15249 + $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
15250 + $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
15251 + // also get the issue title of the duplicated issue
15252 + if (!empty($res['iss_duplicated_iss_id'])) {
15253 + $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
15256 + // get group information
15257 + if (!empty($res["iss_grp_id"])) {
15258 + $res["group"] = Group::getDetails($res["iss_grp_id"]);
15261 + // get quarantine issue
15262 + $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
15264 + $returns[$issue_id] = $res;
15272 + * Method used to get some simple details about the given duplicated issue.
15275 + * @param integer $issue_id The issue ID
15276 + * @return array The duplicated issue details
15278 + function getDuplicatedDetails($issue_id)
15281 + iss_summary title,
15282 + sta_title current_status,
15283 + sta_is_closed is_closed
15285 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15286 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15288 + iss_sta_id=sta_id AND
15289 + iss_id=$issue_id";
15290 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15291 + if (PEAR::isError($res)) {
15292 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15301 + * Method used to bulk update a list of issues
15304 + * @return boolean
15306 + function bulkUpdate()
15308 + // check if user performing this chance has the proper role
15309 + if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
15313 + $items = Misc::escapeInteger($_POST['item']);
15314 + $new_status_id = Misc::escapeInteger($_POST['status']);
15315 + $new_release_id = Misc::escapeInteger(@$_POST['release']);
15316 + $new_priority_id = Misc::escapeInteger($_POST['priority']);
15317 + $new_category_id = Misc::escapeInteger($_POST['category']);
15319 + for ($i = 0; $i < count($items); $i++) {
15320 + if (!self::canAccess($items[$i], Auth::getUserID())) {
15322 + } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
15323 + // make sure issue is not in another project
15327 + $updated_fields = array();
15329 + // update assignment
15330 + if (count(@$_POST['users']) > 0) {
15331 + $users = Misc::escapeInteger($_POST['users']);
15332 + // get who this issue is currently assigned too
15337 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15338 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15340 + isu_usr_id = usr_id AND
15341 + isu_iss_id = " . $items[$i];
15342 + $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
15343 + if (PEAR::isError($current_assignees)) {
15344 + Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
15347 + foreach ($current_assignees as $usr_id => $usr_name) {
15348 + if (!in_array($usr_id, $users)) {
15349 + self::deleteUserAssociation($items[$i], $usr_id, false);
15352 + $new_user_names = array();
15353 + $new_assignees = array();
15354 + foreach ($users as $usr_id) {
15355 + $new_user_names[$usr_id] = User::getFullName($usr_id);
15357 + // check if the issue is already assigned to this person
15359 + COUNT(*) AS total
15361 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
15363 + isu_iss_id=" . $items[$i] . " AND
15364 + isu_usr_id=" . $usr_id;
15365 + $total = DB_Helper::getInstance()->getOne($stmt);
15366 + if ($total > 0) {
15369 + $new_assignees[] = $usr_id;
15370 + // add the assignment
15371 + self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
15372 + Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
15373 + Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
15376 + Notification::notifyNewAssignment($new_assignees, $items[$i]);
15377 + $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
15381 + if (!empty($new_status_id)) {
15382 + $old_status_id = self::getStatusID($items[$i]);
15383 + $res = self::setStatus($items[$i], $new_status_id, false);
15385 + $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
15389 + // update release
15390 + if (!empty($new_release_id)) {
15391 + $old_release_id = self::getRelease($items[$i]);
15392 + $res = self::setRelease($items[$i], $new_release_id);
15394 + $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
15398 + // update priority
15399 + if (!empty($new_priority_id)) {
15400 + $old_priority_id = self::getPriority($items[$i]);
15401 + $res = self::setPriority($items[$i], $new_priority_id);
15403 + $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
15407 + // update category
15408 + if (!empty($new_category_id)) {
15409 + $old_category_id = self::getCategory($items[$i]);
15410 + $res = self::setCategory($items[$i], $new_category_id);
15412 + $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
15416 + if (count($updated_fields) > 0) {
15417 + // log the changes
15420 + foreach ($updated_fields as $key => $value) {
15422 + $changes .= "; ";
15424 + $changes .= "$key: $value";
15427 + History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
15430 + // close if request
15431 + if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
15432 + self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
15440 + * Method used to set the initial impact analysis for a specific issue
15443 + * @param integer $issue_id The issue ID
15444 + * @return integer 1 if the update worked, -1 otherwise
15446 + function setImpactAnalysis($issue_id)
15449 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15451 + iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
15452 + iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
15453 + iss_last_internal_action_type='update',
15454 + iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
15455 + iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
15457 + iss_id=" . Misc::escapeInteger($issue_id);
15458 + $res = DB_Helper::getInstance()->query($stmt);
15459 + if (PEAR::isError($res)) {
15460 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15463 + // add the impact analysis to the history of the issue
15464 + $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
15465 + History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
15472 + * Method used to get the full list of issue IDs that area available in the
15476 + * @param string $extra_condition An extra condition in the WHERE clause
15477 + * @return array The list of issue IDs
15479 + function getColList($extra_condition = NULL)
15484 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15486 + iss_prj_id=" . Auth::getCurrentProject();
15487 + if (!empty($extra_condition)) {
15488 + $stmt .= " AND $extra_condition ";
15493 + $res = DB_Helper::getInstance()->getCol($stmt);
15494 + if (PEAR::isError($res)) {
15495 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15504 + * Method used to get the full list of issue IDs and their respective
15508 + * @param string $extra_condition An extra condition in the WHERE clause
15509 + * @return array The list of issues
15511 + function getAssocList($extra_condition = NULL)
15517 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15519 + iss_prj_id=" . Auth::getCurrentProject();
15520 + if (!empty($extra_condition)) {
15521 + $stmt .= " AND $extra_condition ";
15526 + $res = DB_Helper::getInstance()->getAssoc($stmt);
15527 + if (PEAR::isError($res)) {
15528 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15537 + * Method used to get the list of issues associated to a specific issue.
15540 + * @param integer $issue_id The issue ID
15541 + * @return array The list of associated issues
15543 + function getAssociatedIssues($issue_id)
15545 + $issues = self::getAssociatedIssuesDetails($issue_id);
15546 + $associated = array();
15547 + for ($i = 0; $i < count($issues); $i++) {
15548 + $associated[] = $issues[$i]['associated_issue'];
15550 + return $associated;
15555 + * Method used to get the list of issues associated details to a
15556 + * specific issue.
15559 + * @param integer $issue_id The issue ID
15560 + * @return array The list of associated issues
15562 + function getAssociatedIssuesDetails($issue_id)
15566 + if (!empty($returns[$issue_id])) {
15567 + return $returns[$issue_id];
15571 + isa_associated_id associated_issue,
15572 + iss_summary associated_title,
15573 + sta_title current_status,
15574 + sta_is_closed is_closed
15576 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
15577 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15578 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15580 + isa_associated_id=iss_id AND
15581 + iss_sta_id=sta_id AND
15582 + isa_issue_id=$issue_id";
15583 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15584 + if (PEAR::isError($res)) {
15585 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15588 + $returns[$issue_id] = $res;
15595 + * Method used to check whether an issue was already closed or not.
15598 + * @param integer $issue_id The issue ID
15599 + * @return boolean
15601 + function isClosed($issue_id)
15606 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15607 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15609 + iss_id=" . Misc::escapeInteger($issue_id) . " AND
15610 + iss_sta_id=sta_id AND
15611 + sta_is_closed=1";
15612 + $res = DB_Helper::getInstance()->getOne($stmt);
15613 + if (PEAR::isError($res)) {
15614 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15627 + * Returns a simple list of issues that are currently set to some
15628 + * form of quarantine. This is mainly used by the IRC interface.
15631 + * @return array List of quarantined issues
15633 + function getQuarantinedIssueList()
15635 + // XXX: would be nice to restrict the result list to only one project
15640 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15641 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15643 + iqu_iss_id=iss_id AND
15644 + iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
15645 + iqu_expiration IS NOT NULL";
15646 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15647 + if (PEAR::isError($res)) {
15648 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15651 + self::getAssignedUsersByIssues($res);
15658 + * Returns the status of a quarantine.
15660 + * @param integer $issue_id The issue ID
15661 + * @return integer Indicates what the current state of quarantine is.
15663 + function getQuarantineInfo($issue_id)
15669 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15671 + iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
15672 + (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
15673 + iqu_expiration IS NULL)";
15674 + $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15675 + if (PEAR::isError($res)) {
15676 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15679 + if (!empty($res["iqu_expiration"])) {
15680 + $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
15681 + $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
15689 + * Sets the quarantine status. Optionally an expiration date can be set
15690 + * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
15693 + * @param integer $issue_id The issue ID
15694 + * @param integer $status The quarantine status
15695 + * @param string $expiration The expiration date of quarantine (default empty)
15697 + function setQuarantine($issue_id, $status, $expiration = '')
15699 + $issue_id = Misc::escapeInteger($issue_id);
15700 + $status = Misc::escapeInteger($status);
15702 + // see if there is an existing record
15706 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15708 + iqu_iss_id = $issue_id";
15709 + $res = DB_Helper::getInstance()->getOne($stmt);
15710 + if (PEAR::isError($res)) {
15711 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15717 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15719 + iqu_status = $status";
15720 + if (!empty($expiration)) {
15721 + $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
15723 + $stmt .= "\nWHERE
15724 + iqu_iss_id = $issue_id";
15725 + $res = DB_Helper::getInstance()->query($stmt);
15726 + if (PEAR::isError($res)) {
15727 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15730 + // add history entry about this change taking place
15731 + if ($status == 0) {
15732 + History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
15733 + "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
15738 + $stmt = "INSERT INTO
15739 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15743 + if (!empty($expiration)) {
15744 + $stmt .= ",\niqu_expiration\n";
15746 + $stmt .= ") VALUES (
15749 + if (!empty($expiration)) {
15750 + $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
15753 + $res = DB_Helper::getInstance()->query($stmt);
15754 + if (PEAR::isError($res)) {
15755 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15764 + * Sets the group of the issue.
15767 + * @param integer $issue_id The ID of the issue
15768 + * @param integer $group_id The ID of the group
15769 + * @return integer 1 if successful, -1 or -2 otherwise
15771 + function setGroup($issue_id, $group_id)
15773 + $issue_id = Misc::escapeInteger($issue_id);
15774 + $group_id = Misc::escapeInteger($group_id);
15776 + $current = self::getDetails($issue_id);
15777 + if ($current["iss_grp_id"] == $group_id) {
15781 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15783 + iss_grp_id = $group_id
15785 + iss_id = $issue_id";
15786 + $res = DB_Helper::getInstance()->query($stmt);
15787 + if (PEAR::isError($res)) {
15788 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15791 + $current_user = Auth::getUserID();
15792 + if (empty($current_user)) {
15793 + $current_user = APP_SYSTEM_USER_ID;
15795 + History::add($issue_id, $current_user, History::getTypeID('group_changed'),
15796 + "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
15802 + * Returns the group ID associated with the given issue ID.
15805 + * @param integer $issue_id The issue ID
15806 + * @return integer The associated group ID
15808 + function getGroupID($issue_id)
15813 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15815 + iss_id=" . Misc::escapeInteger($issue_id);
15816 + $res = DB_Helper::getInstance()->getOne($stmt);
15817 + if (PEAR::isError($res)) {
15818 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15827 + * Returns an array of issues based on full text search results.
15829 + * @param array $options An array of search options
15830 + * @return array An array of issue IDS
15832 + function getFullTextIssues($options)
15834 + // check if a list of issues for this full text search is already cached
15835 + $fulltext_string = Session::get('fulltext_string');
15836 + if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
15837 + return Session::get('fulltext_issues');
15840 + // no pre-existing list, generate them
15844 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15846 + MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15849 + DISTINCT(not_iss_id)
15851 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
15853 + MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15856 + DISTINCT(ttr_iss_id)
15858 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
15860 + MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15863 + DISTINCT(phs_iss_id)
15865 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
15867 + MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15870 + DISTINCT(sup_iss_id)
15872 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
15873 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
15875 + sup_id = seb_sup_id AND
15876 + MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15878 + $res = DB_Helper::getInstance()->getCol($stmt);
15879 + if (PEAR::isError($res)) {
15880 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15881 + return array(-1);
15884 + DISTINCT(icf_iss_id)
15886 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
15888 + MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
15889 + $custom_res = DB_Helper::getInstance()->getCol($stmt);
15890 + if (PEAR::isError($custom_res)) {
15891 + Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
15892 + return array(-1);
15894 + $issues = array_merge($res, $custom_res);
15895 + // we kill the query results on purpose to flag that no
15896 + // issues could be found with fulltext search
15897 + if (count($issues) < 1) {
15898 + $issues = array(-1);
15900 + Session::set('fulltext_string', $options['keywords']);
15901 + Session::set('fulltext_issues', $issues);
15908 + * Method to determine if user can access a particular issue
15911 + * @param integer $issue_id The ID of the issue.
15912 + * @param integer $usr_id The ID of the user
15913 + * @return boolean If the user can access the issue
15915 + function canAccess($issue_id, $usr_id)
15919 + if (empty($issue_id)) {
15923 + if (isset($access[$issue_id . "-" . $usr_id])) {
15924 + return $access[$issue_id . "-" . $usr_id];
15927 + $details = self::getDetails($issue_id);
15928 + if (empty($details)) {
15931 + $usr_details = User::getDetails($usr_id);
15932 + $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
15933 + $prj_id = self::getProjectID($issue_id);
15936 + if (empty($usr_role)) {
15937 + // check if they are even allowed to access the project
15939 + } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
15940 + ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
15941 + // check customer permissions
15943 + } elseif ($details['iss_private'] == 1) {
15944 + // check if the issue is even private
15946 + // check role, reporter, assigment and group
15947 + if ($usr_role > User::getRoleID("Developer")) {
15949 + } elseif ($details['iss_usr_id'] == $usr_id) {
15951 + } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
15953 + } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
15954 + ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
15956 + } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
15961 + } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
15962 + ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
15968 + $access[$issue_id . "-" . $usr_id] = $return;
15974 + * Returns true if the specified issue is private, false otherwise
15977 + * @param integer $issue_id The ID of the issue
15978 + * @return boolean If the issue is private or not
15980 + function isPrivate($issue_id)
15984 + if (!isset($returns[$issue_id])) {
15988 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15990 + iss_id=$issue_id";
15991 + $res = DB_Helper::getInstance()->getOne($sql);
15992 + if (PEAR::isError($res)) {
15993 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15997 + $returns[$issue_id] = true;
15999 + $returns[$issue_id] = false;
16003 + return $returns[$issue_id];
16008 + * Clears closed information from an issues.
16011 + * @param integer $issue_id The ID of the issue
16013 + function clearClosed($issue_id)
16016 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16018 + iss_closed_date = null,
16019 + iss_res_id = null
16021 + iss_id=" . Misc::escapeInteger($issue_id);
16022 + $res = DB_Helper::getInstance()->query($stmt);
16023 + if (PEAR::isError($res)) {
16024 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16027 + self::moveOrderForAllUsers($issue_id, 1);
16032 + * Returns the message ID that should be used as the parent ID for all messages
16035 + * @param integer $issue_id The ID of the issue
16037 + function getRootMessageID($issue_id)
16040 + iss_root_message_id
16042 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16044 + iss_id=" . Misc::escapeInteger($issue_id);
16045 + $res = DB_Helper::getInstance()->getOne($sql);
16046 + if (PEAR::isError($res)) {
16047 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16056 + * Returns the issue ID of the issue with the specified root message ID, or false
16058 + * @param string $msg_id The Message ID
16059 + * @return integer The ID of the issue
16061 + function getIssueByRootMessageID($msg_id)
16065 + if (!empty($returns[$msg_id])) {
16066 + return $returns[$msg_id];
16071 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16073 + iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
16074 + $res = DB_Helper::getInstance()->getOne($sql);
16075 + if (PEAR::isError($res)) {
16076 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16079 + if (empty($res)) {
16080 + $returns[$msg_id] = false;
16082 + $returns[$msg_id] = $res;
16084 + return $returns[$msg_id];
16089 + * Sets the assignees for the issue
16091 + * @param integer $issue_id
16092 + * @param array $assignees
16094 + function setAssignees($issue_id, $assignees)
16096 + if (!is_array($assignees)) {
16097 + $assignees = array();
16100 + // see if there is anything to change
16101 + $old_assignees = self::getAssignedUserIDs($issue_id);
16102 + if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
16106 + $old_assignee_names = self::getAssignedUsers($issue_id);
16108 + Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
16109 + // clear up the assignments for this issue, and then assign it to the current user
16110 + self::deleteUserAssociations($issue_id);
16111 + $assignee_names = array();
16112 + foreach ($assignees as $assignee) {
16113 + $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
16114 + if ($res == -1) {
16117 + $assignee_names[] = User::getFullName($assignee);
16118 + Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
16121 + Notification::notifyNewAssignment($assignees, $issue_id);
16123 + // save a history entry about this...
16124 + History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
16125 + "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
16129 + * Reorders user's issues as requested by user
16131 + * @param $usr_id User to be reordered
16132 + * @param $issue_id Issue or array of issues to be moved
16133 + * @param $neworder The new order of the issues
16136 + function reorderUserIssues($usr_id, $issue_id, $neworder)
16138 + if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
16141 + if (!is_numeric($usr_id) || !is_numeric($neworder)) {
16144 + $usr_id = Misc::escapeInteger($usr_id);
16145 + $issue_id = Misc::escapeInteger($issue_id);
16146 + $neworder = Misc::escapeInteger($neworder);
16147 + if (is_array($issue_id)) {
16148 + $issue_count = count($issue_id);
16149 + $issue_id_str = implode(", ", $issue_id);
16151 + $issue_count = 1;
16152 + $issue_id_str = $issue_id;
16153 + $issue_id = array($issue_id);
16155 + // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
16156 + $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
16157 + self::rearrangeDeleteUserAssociationOrderList($orderlist);
16158 + // move down the orders to free the "order space" needed
16160 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16162 + isu_order = isu_order + $issue_count
16164 + isu_usr_id = $usr_id AND
16165 + isu_order >= $neworder";
16166 + $res = DB_Helper::getInstance()->query($stmt);
16167 + if (PEAR::isError($res)) {
16168 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16171 + //update the order for the issues being moved
16173 + foreach ($issue_id as $iss_id) {
16175 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16177 + isu_order = " . ($neworder + $i) . "
16179 + isu_usr_id = $usr_id AND
16180 + isu_iss_id = $iss_id";
16181 + $res = DB_Helper::getInstance()->query($stmt);
16182 + if (PEAR::isError($res)) {
16183 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16192 + * Get users issue order list
16194 + * @param $user_id User
16195 + * @param $order_list Order of the issues
16198 + function getIssueOrderByUser($usr_id) {
16200 + if (!is_numeric($usr_id)) {
16205 + isu_iss_id, isu_order
16207 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16209 + isu_usr_id = " . $usr_id ;
16211 + $order_list = array();
16213 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
16215 + if (PEAR::isError($res)) {
16216 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16219 + foreach ($res as $row) {
16220 + $order_list[$row["isu_iss_id"]] = $row["isu_order"];
16223 + return $order_list;
16226 + function moveOrderForAllUsers($issue_id, $neworder)
16228 + // Move the issue to the top priority for the ppl it's assigned to
16229 + $stmt = "SELECT isu_usr_id FROM
16230 + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16232 + isu_iss_id = " . Misc::escapeInteger($issue_id);
16233 + $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
16234 + if (PEAR::isError($res)) {
16235 + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16238 + foreach ($res as $row) {
16239 + self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
16244 --- eventum-2.2/templates/header.tpl.html 2009-09-14 18:07:55.000000000 +0300
16245 +++ eventum-2.2-order/templates/header.tpl.html 2009-10-12 22:10:36.435851675 +0300
16247 <script type="text/javascript" src="{$rel_url}js/jquery/form.js?c=9984"></script>
16248 <script type="text/javascript" src="{$rel_url}js/jquery/blockui.js?c=eb13"></script>
16249 <script type="text/javascript" src="{$rel_url}js/jquery/ui.datepicker.js?c=a911"></script>
16250 +<script type="text/javascript" src="{$rel_url}js/jquery/jquery.tablednd.js"></script>
16251 <link rel="stylesheet" href="{$rel_url}js/jquery/ui.datepicker.css?c=5096">
16252 <script type="text/javascript">
16254 --- eventum-2.2/templates/list.tpl.html 2009-09-14 18:07:55.000000000 +0300
16255 +++ eventum-2.2-order/templates/list.tpl.html 2009-10-12 22:10:36.439185157 +0300
16257 f.target = '_popup';
16260 +function reorderBulk(order_user, neworder)
16262 + url = page_url + "?";
16263 + url += "reorder_user=" + order_user;
16265 + items = document.getElementsByName("item[]");
16266 + checkedcount = 0;
16267 + for (var i = 0; i < items.length; i++) {
16268 + if (items[i].checked) {
16269 + url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
16273 + if (checkedcount == 0) {
16274 + alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
16278 + url += "&reorder_neworder=" + neworder;
16280 + window.location.href = url;
16282 function hideClosed(f)
16284 if (f.hide_closed.checked) {
16285 @@ -153,6 +175,13 @@
16286 f.go.disabled = true;
16289 +function updateCustomFields(issue_id)
16291 + var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16292 + var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
16293 + customWin.focus();
16299 @@ -169,11 +198,11 @@
16300 <input type="hidden" name="cat" value="bulk_update">
16303 - <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
16305 + <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
16306 + <tr class="nodrag">
16307 <td colspan="{$col_count}" class="default">
16308 <table width="100%" cellspacing="0" cellpadding="0" border="0">
16310 + <tr class="nodrag">
16311 <td class="default">
16312 <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
16313 {include file="help_link.tpl.html" topic="list"}
16314 @@ -193,7 +222,7 @@
16318 - <tr bgcolor="{$cell_color}">
16319 + <tr bgcolor="{$cell_color}" class="nodrag">
16320 {if $current_role > $roles.developer}
16322 <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
16323 @@ -208,7 +237,7 @@
16324 {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
16328 + {elseif $field_name != 'isu_order' || $isu_order_user}
16329 <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
16330 {if $field_name == 'iss_summary'}
16331 <table cellspacing="0" cellpadding="1" width="100%">
16332 @@ -232,20 +261,21 @@
16337 {section name="i" loop=$list}
16338 - <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
16339 + <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
16340 {if $current_role > $roles.developer}
16341 <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
16343 {foreach from=$columns item=column key=field_name}
16344 {if $field_name == 'custom_fields'}
16345 {foreach from=$list[i].custom_field key=fld_id item=fld_value}
16346 - <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16347 - {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16348 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
16349 + {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16353 - <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16354 + {elseif $field_name != 'isu_order' || $isu_order_user}
16355 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default{if $field_name == 'isu_order'} dragHandle{/if}">
16356 {if $field_name == 'iss_id'}
16357 <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
16358 {elseif $field_name == 'pri_rank'}
16359 @@ -279,7 +309,7 @@
16360 {elseif $field_name == 'iss_percent_complete'}
16361 {$list[i].iss_percent_complete|escape:"html"}%
16362 {elseif $field_name == 'iss_expected_resolution_date'}
16363 - {$list[i].iss_expected_resolution_date|escape:"html"}
16364 + <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"} </div>
16365 {elseif $field_name == 'iss_summary'}
16366 <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
16367 {if $list[i].redeemed}
16368 @@ -288,6 +318,8 @@
16369 {if $list[i].iss_private == 1}
16372 + {elseif $field_name == 'isu_order'}
16373 + {if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user} {/if}
16377 @@ -300,10 +332,11 @@
16381 - <tr bgcolor="{$cell_color}">
16383 + <tr bgcolor="{$cell_color}" class="nodrag">
16384 <td colspan="{$col_count}">
16385 <table width="100%" cellspacing="0" cellpadding="0">
16387 + <tr class="nodrag">
16388 <td width="30%" nowrap>
16389 {if $current_role > $roles.developer}
16390 <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
16391 @@ -355,6 +388,35 @@
16396 +<script type="text/javascript">
16398 + * Order issues by drag and drop:
16399 + * only if sorted by order and viewing your own issues
16401 +{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
16403 +var before = ''; // make it global variable
16404 +$('#issue_list_table').tableDnD({
16405 + onDragClass: "tDnD_whileDrag",
16406 + onDragStart: function(table, row) {
16407 + before = $.tableDnD.serialize('id');
16409 + onDrop: function(table, row) {
16410 + $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
16411 + if (data.length > 0) {
16416 + dragHandle: "dragHandle"
16418 +$("#issue_list_table tr").hover(function() {
16419 + $('#' + this.id + ' .dragHandle').addClass('showDragHandle');
16421 + $('#' + this.id + ' .dragHandle').removeClass('showDragHandle');
16426 {include file="app_info.tpl.html"}
16427 {include file="footer.tpl.html"}
16428 --- eventum-2.2/templates/list.tpl.html.~1~ 1970-01-01 02:00:00.000000000 +0200
16429 +++ eventum-2.2-order/templates/list.tpl.html.~1~ 2009-09-14 18:07:55.000000000 +0300
16431 +{include file="header.tpl.html" extra_title="List of Issues"}
16432 +{include file="navigation.tpl.html"}
16434 +{if $current_role != $roles.customer}
16435 +{include file="quick_filter_form.tpl.html"}
16436 +{include file="current_filters.tpl.html"}
16438 +<script type="text/javascript">
16440 +var page_url = '{$smarty.server.PHP_SELF}';
16441 +var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
16442 +var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
16444 +// this function will display the bulk update form if any issues are selected.
16445 +// if no issues are selected it will be hidden.
16446 +function toggleBulkUpdate()
16448 + var items = document.getElementsByName('item[]');
16450 + // loop through looking to see which are checked
16451 + var show = false;
16452 + for (var i = 0; i < items.length; i++) {
16453 + if (items[i].checked) {
16459 + changeVisibility('bulk_update1', show);
16462 +function resetBulkUpdate()
16464 + var f = getForm('list_form');
16465 + clearSelectedOptions(getFormElement(f, 'users[]'));
16466 + clearSelectedOptions(getFormElement(f, 'status'));
16467 + if (getFormElement(f, 'release')) {
16468 + clearSelectedOptions(getFormElement(f, 'release'));
16471 +function bulkUpdate()
16473 + var f = getForm('list_form');
16474 + if (!hasOneChecked(f, 'item[]')) {
16475 + alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
16479 + // figure out what is changing
16480 + var changed = new Array();
16481 + if (hasOneSelected(f, 'users[]')) {
16482 + changed[changed.length] = 'Assignment';
16484 + if (f.elements.status.selectedIndex != 0) {
16485 + changed[changed.length] = 'Status';
16487 + if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
16488 + changed[changed.length] = 'Release';
16490 + if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
16491 + changed[changed.length] = 'Priority';
16493 + if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
16494 + changed[changed.length] = 'Category';
16496 + if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
16497 + changed[changed.length] = 'Closed Status';
16499 + if (changed.length < 1) {
16500 + alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
16503 + var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
16504 + for (var i = 0; i < changed.length; i++) {
16505 + msg += changed[i];
16506 + if ((changed.length > 1) && (i == (changed.length-2))) {
16509 + if (i != (changed.length-1)) {
16514 + msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
16515 + if (!confirm(msg)) {
16518 + var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16519 + var popupWin = window.open('', '_popup', features);
16520 + popupWin.focus();
16521 + f.action = 'popup.php';
16522 + f.target = '_popup';
16525 +function hideClosed(f)
16527 + if (f.hide_closed.checked) {
16528 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
16530 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
16533 +function resizePager(f)
16535 + var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
16536 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
16538 +function checkPageField(ev)
16540 + // check if the user is trying to submit the form by hitting <enter>
16541 + if (((window.event) && (window.event.keyCode == 13)) ||
16542 + ((ev) && (ev.which == 13))) {
16546 +function goPage(f, new_page)
16548 + if ((new_page > last_page+1) || (new_page <= 0) ||
16549 + (new_page == current_page+1) || (!isNumberOnly(new_page))) {
16550 + f.page.value = current_page+1;
16553 + setPage(new_page-1);
16555 +function setPage(new_page)
16557 + if ((new_page > last_page) || (new_page < 0) ||
16558 + (new_page == current_page)) {
16561 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
16563 +function downloadCSV()
16565 + var f = this.document.csv_form;
16569 +window.onload = disableFields;
16570 +function disableFields()
16572 + var f = document.list_form;
16573 + if (current_page == 0) {
16574 + f.first.disabled = true;
16575 + f.previous.disabled = true;
16577 + if ((current_page == last_page) || (last_page <= 0)) {
16578 + f.next.disabled = true;
16579 + f.last.disabled = true;
16581 + if ((current_page == 0) && (last_page <= 0)) {
16582 + f.page.disabled = true;
16583 + f.go.disabled = true;
16589 +{* Figure out how many columns there are *}
16590 +{assign var="col_count" value=$columns|@count}
16591 +{if $list_info.custom_fields|@count > 1}
16592 +{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
16594 +{if $current_role > $roles.developer}
16595 + {math assign="col_count" equation="x+1" x=$col_count}
16597 +<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
16598 + <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
16599 + <input type="hidden" name="cat" value="bulk_update">
16602 + <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
16604 + <td colspan="{$col_count}" class="default">
16605 + <table width="100%" cellspacing="0" cellpadding="0" border="0">
16607 + <td class="default">
16608 + <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
16609 + {include file="help_link.tpl.html" topic="list"}
16611 + {if $current_role != $roles.customer}
16612 + <td align="right" class="default" nowrap>
16613 + {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
16614 + {t}hide/show{/t}:
16615 + [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
16616 + {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
16617 + {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
16618 + {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
16626 + <tr bgcolor="{$cell_color}">
16627 + {if $current_role > $roles.developer}
16629 + <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
16632 + {foreach from=$columns item=column key=field_name}
16633 + {if $field_name == 'custom_fields'}
16634 + {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
16635 + {assign var="fld_name_id" value="custom_field_`$fld_id`"}
16636 + <td align="{$column.align|default:'center'}" class="default_white" nowrap>
16637 + <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
16638 + {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
16642 + <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
16643 + {if $field_name == 'iss_summary'}
16644 + <table cellspacing="0" cellpadding="1" width="100%">
16646 + <td class="default_white">
16647 + <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
16648 + {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
16650 + <td align="right">
16651 + <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
16655 + {elseif $sorting.links[$field_name] != ''}
16656 + <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
16657 + {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
16665 + {section name="i" loop=$list}
16666 + <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
16667 + {if $current_role > $roles.developer}
16668 + <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
16670 + {foreach from=$columns item=column key=field_name}
16671 + {if $field_name == 'custom_fields'}
16672 + {foreach from=$list[i].custom_field key=fld_id item=fld_value}
16673 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16674 + {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16678 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16679 + {if $field_name == 'iss_id'}
16680 + <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
16681 + {elseif $field_name == 'pri_rank'}
16682 + {$list[i].pri_title|escape:"html"}
16683 + {elseif $field_name == 'iss_grp_id'}
16685 + {elseif $field_name == 'assigned'}
16686 + {$list[i].assigned_users}
16687 + {elseif $field_name == 'time_spent'}
16688 + {$list[i].time_spent}
16689 + {elseif $field_name == 'prc_title'}
16690 + {$list[i].prc_title}
16691 + {elseif $field_name == 'pre_title'}
16692 + {$list[i].pre_title|escape:"html"}
16693 + {elseif $field_name == 'iss_customer_id'}
16694 + {$list[i].customer_title|escape:"html"}
16695 + {elseif $field_name == 'support_level'}
16696 + {$list[i].support_level|escape:"html"}
16697 + {elseif $field_name == 'sta_rank'}
16698 + {$list[i].sta_title|escape:"html"}
16699 + {elseif $field_name == 'iss_created_date'}
16700 + {$list[i].iss_created_date|escape:"html"}
16701 + {elseif $field_name == 'iss_dev_time'}
16702 + {$list[i].iss_dev_time|escape:"html"}
16703 + {elseif $field_name == 'sta_change_date'}
16704 + {$list[i].status_change_date|escape:"html"}
16705 + {elseif $field_name == 'last_action_date'}
16706 + {$list[i].last_action_date|escape:"html"}
16707 + {elseif $field_name == 'usr_full_name'}
16708 + {$list[i].usr_full_name|escape:"html"}
16709 + {elseif $field_name == 'iss_percent_complete'}
16710 + {$list[i].iss_percent_complete|escape:"html"}%
16711 + {elseif $field_name == 'iss_expected_resolution_date'}
16712 + {$list[i].iss_expected_resolution_date|escape:"html"}
16713 + {elseif $field_name == 'iss_summary'}
16714 + <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
16715 + {if $list[i].redeemed}
16718 + {if $list[i].iss_private == 1}
16727 + <tr bgcolor="gray">
16728 + <td colspan="{$col_count}" class="default_white" align="center">
16729 + <i>{t}No issues could be found.{/t}</i>
16733 + <tr bgcolor="{$cell_color}">
16734 + <td colspan="{$col_count}">
16735 + <table width="100%" cellspacing="0" cellpadding="0">
16737 + <td width="30%" nowrap>
16738 + {if $current_role > $roles.developer}
16739 + <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
16742 + <td width="40%" align="center" nowrap>
16744 + <input name="first" type="button" value="|<" class="shortcut" onClick="javascript:setPage(0);">
16745 + <input name="previous" type="button" value="<<" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
16746 + <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
16747 + <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
16748 + <input name="next" type="button" value=">>" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
16749 + <input name="last" type="button" value=">|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
16753 + <span class="default_white">{t}Rows per Page:{/t}</span>
16754 + <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
16755 + <option value="5" {if $options.rows == 5}selected{/if}>5</option>
16756 + <option value="10" {if $options.rows == 10}selected{/if}>10</option>
16757 + <option value="25" {if $options.rows == 25}selected{/if}>25</option>
16758 + <option value="50" {if $options.rows == 50}selected{/if}>50</option>
16759 + <option value="100" {if $options.rows == 100}selected{/if}>100</option>
16760 + <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
16762 + <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
16764 + <td width="30%" class="default_white" align="right">
16765 + <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>
16774 + {if $current_role > $roles.developer}
16776 + <td bgcolor="#FFFFFF">
16778 + {include file="bulk_update.tpl.html"}
16783 + <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
16784 + <input type="hidden" name="csv_data" value="{$csv_data}">
16789 +{include file="app_info.tpl.html"}
16790 +{include file="footer.tpl.html"}
16791 --- eventum-2.2/templates/list.tpl.html.~2~ 1970-01-01 02:00:00.000000000 +0200
16792 +++ eventum-2.2-order/templates/list.tpl.html.~2~ 2009-10-12 22:10:36.435851675 +0300
16794 +{include file="header.tpl.html" extra_title="List of Issues"}
16795 +{include file="navigation.tpl.html"}
16797 +{if $current_role != $roles.customer}
16798 +{include file="quick_filter_form.tpl.html"}
16799 +{include file="current_filters.tpl.html"}
16801 +<script type="text/javascript">
16803 +var page_url = '{$smarty.server.PHP_SELF}';
16804 +var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
16805 +var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
16807 +// this function will display the bulk update form if any issues are selected.
16808 +// if no issues are selected it will be hidden.
16809 +function toggleBulkUpdate()
16811 + var items = document.getElementsByName('item[]');
16813 + // loop through looking to see which are checked
16814 + var show = false;
16815 + for (var i = 0; i < items.length; i++) {
16816 + if (items[i].checked) {
16822 + changeVisibility('bulk_update1', show);
16825 +function resetBulkUpdate()
16827 + var f = getForm('list_form');
16828 + clearSelectedOptions(getFormElement(f, 'users[]'));
16829 + clearSelectedOptions(getFormElement(f, 'status'));
16830 + if (getFormElement(f, 'release')) {
16831 + clearSelectedOptions(getFormElement(f, 'release'));
16834 +function bulkUpdate()
16836 + var f = getForm('list_form');
16837 + if (!hasOneChecked(f, 'item[]')) {
16838 + alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
16842 + // figure out what is changing
16843 + var changed = new Array();
16844 + if (hasOneSelected(f, 'users[]')) {
16845 + changed[changed.length] = 'Assignment';
16847 + if (f.elements.status.selectedIndex != 0) {
16848 + changed[changed.length] = 'Status';
16850 + if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
16851 + changed[changed.length] = 'Release';
16853 + if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
16854 + changed[changed.length] = 'Priority';
16856 + if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
16857 + changed[changed.length] = 'Category';
16859 + if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
16860 + changed[changed.length] = 'Closed Status';
16862 + if (changed.length < 1) {
16863 + alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
16866 + var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
16867 + for (var i = 0; i < changed.length; i++) {
16868 + msg += changed[i];
16869 + if ((changed.length > 1) && (i == (changed.length-2))) {
16872 + if (i != (changed.length-1)) {
16877 + msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
16878 + if (!confirm(msg)) {
16881 + var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16882 + var popupWin = window.open('', '_popup', features);
16883 + popupWin.focus();
16884 + f.action = 'popup.php';
16885 + f.target = '_popup';
16888 +function reorderBulk(order_user, neworder)
16890 + url = page_url + "?";
16891 + url += "reorder_user=" + order_user;
16893 + items = document.getElementsByName("item[]");
16894 + checkedcount = 0;
16895 + for (var i = 0; i < items.length; i++) {
16896 + if (items[i].checked) {
16897 + url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
16901 + if (checkedcount == 0) {
16902 + alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
16906 + url += "&reorder_neworder=" + neworder;
16908 + window.location.href = url;
16910 +function hideClosed(f)
16912 + if (f.hide_closed.checked) {
16913 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
16915 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
16918 +function resizePager(f)
16920 + var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
16921 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
16923 +function checkPageField(ev)
16925 + // check if the user is trying to submit the form by hitting <enter>
16926 + if (((window.event) && (window.event.keyCode == 13)) ||
16927 + ((ev) && (ev.which == 13))) {
16931 +function goPage(f, new_page)
16933 + if ((new_page > last_page+1) || (new_page <= 0) ||
16934 + (new_page == current_page+1) || (!isNumberOnly(new_page))) {
16935 + f.page.value = current_page+1;
16938 + setPage(new_page-1);
16940 +function setPage(new_page)
16942 + if ((new_page > last_page) || (new_page < 0) ||
16943 + (new_page == current_page)) {
16946 + window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
16948 +function downloadCSV()
16950 + var f = this.document.csv_form;
16954 +window.onload = disableFields;
16955 +function disableFields()
16957 + var f = document.list_form;
16958 + if (current_page == 0) {
16959 + f.first.disabled = true;
16960 + f.previous.disabled = true;
16962 + if ((current_page == last_page) || (last_page <= 0)) {
16963 + f.next.disabled = true;
16964 + f.last.disabled = true;
16966 + if ((current_page == 0) && (last_page <= 0)) {
16967 + f.page.disabled = true;
16968 + f.go.disabled = true;
16971 +function updateCustomFields(issue_id)
16973 + var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16974 + var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
16975 + customWin.focus();
16981 +{* Figure out how many columns there are *}
16982 +{assign var="col_count" value=$columns|@count}
16983 +{if $list_info.custom_fields|@count > 1}
16984 +{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
16986 +{if $current_role > $roles.developer}
16987 + {math assign="col_count" equation="x+1" x=$col_count}
16989 +<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
16990 + <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
16991 + <input type="hidden" name="cat" value="bulk_update">
16994 + <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
16995 + <tr class="nodrag">
16996 + <td colspan="{$col_count}" class="default">
16997 + <table width="100%" cellspacing="0" cellpadding="0" border="0">
16998 + <tr class="nodrag">
16999 + <td class="default">
17000 + <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
17001 + {include file="help_link.tpl.html" topic="list"}
17003 + {if $current_role != $roles.customer}
17004 + <td align="right" class="default" nowrap>
17005 + {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
17006 + {t}hide/show{/t}:
17007 + [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
17008 + {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
17009 + {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
17010 + {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
17018 + <tr bgcolor="{$cell_color}" class="nodrag">
17019 + {if $current_role > $roles.developer}
17021 + <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
17024 + {foreach from=$columns item=column key=field_name}
17025 + {if $field_name == 'custom_fields'}
17026 + {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
17027 + {assign var="fld_name_id" value="custom_field_`$fld_id`"}
17028 + <td align="{$column.align|default:'center'}" class="default_white" nowrap>
17029 + <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
17030 + {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
17033 + {elseif $field_name != 'isu_order' || $isu_order_user}
17034 + <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
17035 + {if $field_name == 'iss_summary'}
17036 + <table cellspacing="0" cellpadding="1" width="100%">
17038 + <td class="default_white">
17039 + <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
17040 + {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
17042 + <td align="right">
17043 + <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
17047 + {elseif $sorting.links[$field_name] != ''}
17048 + <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
17049 + {if $field_name == 'isu_order'}
17050 + <br>{$users[$isu_order_user]}
17052 + {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
17061 + {section name="i" loop=$list}
17062 + <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
17063 + {if $current_role > $roles.developer}
17064 + <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
17066 + {foreach from=$columns item=column key=field_name}
17067 + {if $field_name == 'custom_fields'}
17068 + {foreach from=$list[i].custom_field key=fld_id item=fld_value}
17069 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
17070 + {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
17073 + {elseif $field_name != 'isu_order' || $isu_order_user}
17074 + <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
17075 + {if $field_name == 'iss_id'}
17076 + <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
17077 + {elseif $field_name == 'pri_rank'}
17078 + {$list[i].pri_title|escape:"html"}
17079 + {elseif $field_name == 'iss_grp_id'}
17081 + {elseif $field_name == 'assigned'}
17082 + {$list[i].assigned_users}
17083 + {elseif $field_name == 'time_spent'}
17084 + {$list[i].time_spent}
17085 + {elseif $field_name == 'prc_title'}
17086 + {$list[i].prc_title}
17087 + {elseif $field_name == 'pre_title'}
17088 + {$list[i].pre_title|escape:"html"}
17089 + {elseif $field_name == 'iss_customer_id'}
17090 + {$list[i].customer_title|escape:"html"}
17091 + {elseif $field_name == 'support_level'}
17092 + {$list[i].support_level|escape:"html"}
17093 + {elseif $field_name == 'sta_rank'}
17094 + {$list[i].sta_title|escape:"html"}
17095 + {elseif $field_name == 'iss_created_date'}
17096 + {$list[i].iss_created_date|escape:"html"}
17097 + {elseif $field_name == 'iss_dev_time'}
17098 + {$list[i].iss_dev_time|escape:"html"}
17099 + {elseif $field_name == 'sta_change_date'}
17100 + {$list[i].status_change_date|escape:"html"}
17101 + {elseif $field_name == 'last_action_date'}
17102 + {$list[i].last_action_date|escape:"html"}
17103 + {elseif $field_name == 'usr_full_name'}
17104 + {$list[i].usr_full_name|escape:"html"}
17105 + {elseif $field_name == 'iss_percent_complete'}
17106 + {$list[i].iss_percent_complete|escape:"html"}%
17107 + {elseif $field_name == 'iss_expected_resolution_date'}
17108 + <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"} </div>
17109 + {elseif $field_name == 'iss_summary'}
17110 + <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
17111 + {if $list[i].redeemed}
17114 + {if $list[i].iss_private == 1}
17117 + {elseif $field_name == 'isu_order'}
17118 + {if $list[i].assigned_users_order[$current_user_id]}
17119 + <img src="{$rel_url}images/updown.gif" alt="move">
17127 + <tr bgcolor="gray">
17128 + <td colspan="{$col_count}" class="default_white" align="center">
17129 + <i>{t}No issues could be found.{/t}</i>
17134 + <tr bgcolor="{$cell_color}" class="nodrag">
17135 + <td colspan="{$col_count}">
17136 + <table width="100%" cellspacing="0" cellpadding="0">
17137 + <tr class="nodrag">
17138 + <td width="30%" nowrap>
17139 + {if $current_role > $roles.developer}
17140 + <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
17143 + <td width="40%" align="center" nowrap>
17145 + <input name="first" type="button" value="|<" class="shortcut" onClick="javascript:setPage(0);">
17146 + <input name="previous" type="button" value="<<" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
17147 + <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
17148 + <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
17149 + <input name="next" type="button" value=">>" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
17150 + <input name="last" type="button" value=">|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
17154 + <span class="default_white">{t}Rows per Page:{/t}</span>
17155 + <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
17156 + <option value="5" {if $options.rows == 5}selected{/if}>5</option>
17157 + <option value="10" {if $options.rows == 10}selected{/if}>10</option>
17158 + <option value="25" {if $options.rows == 25}selected{/if}>25</option>
17159 + <option value="50" {if $options.rows == 50}selected{/if}>50</option>
17160 + <option value="100" {if $options.rows == 100}selected{/if}>100</option>
17161 + <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
17163 + <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
17165 + <td width="30%" class="default_white" align="right">
17166 + <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>
17175 + {if $current_role > $roles.developer}
17177 + <td bgcolor="#FFFFFF">
17179 + {include file="bulk_update.tpl.html"}
17184 + <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
17185 + <input type="hidden" name="csv_data" value="{$csv_data}">
17189 +<script type="text/javascript">
17191 + * Order issues by drag and drop:
17192 + * only if sorted by order and viewing your own issues
17194 +{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
17196 +var before = ''; // make it global variable
17197 +$('#issue_list_table').tableDnD({
17198 + onDragClass: "tDnD_whileDrag",
17199 + onDragStart: function(table, row) {
17200 + before = $.tableDnD.serialize('id');
17202 + onDrop: function(table, row) {
17203 + $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
17204 + if (data.length > 0) {
17213 +{include file="app_info.tpl.html"}
17214 +{include file="footer.tpl.html"}
17215 \ No newline at end of file
17216 --- eventum-2.2/upgrade/patches/04_isu_order.php 1970-01-01 02:00:00.000000000 +0200
17217 +++ eventum-2.2-order/upgrade/patches/04_isu_order.php 2009-10-12 22:10:36.439185157 +0300
17221 +function db_patch_4() {
17222 + $stmts = array();
17224 + $columns = db_getCol('DESC %TABLE_PREFIX%issue_user');
17225 + if (in_array('isu_order', $columns)) {
17229 + $stmts[] = "ALTER TABLE %TABLE_PREFIX%issue_user ADD isu_order int(11) NOT NULL DEFAULT '0' AFTER isu_assigned_date, ADD INDEX isu_order (isu_order)";
17230 + $stmts[] = "UPDATE %TABLE_PREFIX%issue_user set isu_order=isu_iss_id";
17234 --- eventum-2.2/upgrade/update-database.php 2009-09-14 18:07:55.000000000 +0300
17235 +++ eventum-2.2-order/upgrade/update-database.php 2009-10-12 22:10:36.439185157 +0300
17237 1 => '01_notes.php',
17238 2 => '02_usr_alias.php',
17239 3 => '03_prj_mail_aliases.php',
17240 + 4 => '04_isu_order.php',
17243 // sanity check. check that the version table exists.
17244 --- eventum-2.2/upgrade/update-database.php.~1~ 1970-01-01 02:00:00.000000000 +0200
17245 +++ eventum-2.2-order/upgrade/update-database.php.~1~ 2009-09-14 18:07:55.000000000 +0300
17249 +require_once 'init.php';
17251 +// on fresh install config is empty or missing
17252 +if (!defined('APP_SQL_DBNAME')) {
17253 + fwrite(STDERR, "Eventum not configured. Please run setup.\n");
17257 +define('EXIT_OK', 0);
17258 +define('EXIT_ERROR', 1);
17260 +function db_getAll($query) {
17261 + $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17262 + $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17264 + $res = DB_Helper::getInstance()->getAll($query, DB_FETCHMODE_ASSOC);
17265 + if (PEAR::isError($res)) {
17266 + echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17272 +function db_getOne($query) {
17273 + $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17274 + $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17276 + $res = DB_Helper::getInstance()->getOne($query);
17277 + if (PEAR::isError($res)) {
17278 + echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17284 +function db_getCol($query) {
17285 + $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17286 + $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17288 + $res = DB_Helper::getInstance()->getCol($query);
17289 + if (PEAR::isError($res)) {
17290 + echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17296 +function db_query($query) {
17297 + $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17298 + $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17300 + $res = DB_Helper::getInstance()->query($query);
17301 + if (PEAR::isError($res)) {
17302 + echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17308 +function apply_db_changes($stmts) {
17309 + foreach ($stmts as $stmt) {
17314 +function patch_database() {
17316 + * database versions. each version script can create it's dynamic queries
17318 + $versions = array(
17319 + 1 => '01_notes.php',
17320 + 2 => '02_usr_alias.php',
17321 + 3 => '03_prj_mail_aliases.php',
17324 + // sanity check. check that the version table exists.
17325 + $version = db_getOne("SELECT ver_version FROM %TABLE_PREFIX%version");
17326 + if (!isset($version)) {
17327 + # insert initial value
17328 + db_query("INSERT INTO %TABLE_PREFIX%version SET ver_version=0");
17331 + $target = max(array_keys($versions));
17332 + echo "Current database version: $version; Versions available: $target\n";
17333 + if ($target < $version) {
17334 + echo "ERROR: Your database version is greater ($version) than this upgrade supports ($target)!\n";
17335 + return EXIT_ERROR;
17337 + if ($target == $version) {
17338 + echo "Database already at version $version. Nothing to upgrade.\n";
17342 + echo "Upgrading database to version $target\n";
17343 + for ($i = $version + 1; $i <= $target; $i++) {
17344 + if (empty($versions[$i])) {
17345 + echo "ERROR: patch $i is not recorded in upgrade script.\n";
17346 + return EXIT_ERROR;
17348 + $patch = APP_SQL_PATCHES_PATH . '/' . $versions[$i];
17349 + echo "Checking patch $patch\n";
17350 + if (!file_exists($patch)) {
17351 + echo "ERROR: Patch file doesn't exist\n";
17352 + return EXIT_ERROR;
17355 + $func = "db_patch_$i";
17356 + if (!function_exists($func)) {
17357 + echo "ERROR: Patch did not define '$func' function\n";
17358 + return EXIT_ERROR;
17360 + $patchset = $func();
17361 + echo "Applying patch ", $i, ": ", count($patchset), " queries\n";
17362 + apply_db_changes($patchset);
17363 + db_query("UPDATE %TABLE_PREFIX%version SET ver_version=$i");
17369 +if (php_sapi_name() != 'cli') {
17373 +$ret = patch_database();
17375 +if (php_sapi_name() != 'cli') {