<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
// +----------------------------------------------------------------------+
// | Eventum - Issue Tracking System |
// +----------------------------------------------------------------------+
// | Copyright (c) 2003 - 2008 MySQL AB |
// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
// | |
// | This program is free software; you can redistribute it and/or modify |
// | it under the terms of the GNU General Public License as published by |
// | the Free Software Foundation; either version 2 of the License, or |
// | (at your option) any later version. |
// | |
// | This program is distributed in the hope that it will be useful, |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
// | GNU General Public License for more details. |
// | |
// | You should have received a copy of the GNU General Public License |
// | along with this program; if not, write to: |
// | |
// | Free Software Foundation, Inc. |
// | 59 Temple Place - Suite 330 |
// | Boston, MA 02111-1307, USA. |
// +----------------------------------------------------------------------+
// | Authors: João Prado Maia <hide@address.com> |
// +----------------------------------------------------------------------+
//
// @(#) $Id: class.round_robin.php 3797 2009-01-12 20:14:39Z balsdorf $
//
require_once(APP_INC_PATH . 'class.date.php');
require_once(APP_INC_PATH . 'class.error_handler.php');
class Round_Robin
{
/**
* Returns the blackout dates according to the user's timezone.
*
* @access public
* @param object $user The Date object associated with the user's timezone
* @param integer $start The blackout start hour
* @param integer $end The blackout end hour
* @return array The blackout dates
*/
function getBlackoutDates(&$user, $start, $end)
{
$start = substr($start, 0, 2);
$end = substr($end, 0, 2);
// if start is AM and end is PM, then use only today
// if start is AM and end is AM (and end is smaller than start), then use today and tomorrow
// - if date is between zero and the end, then use yesterday and today
// if start is AM and end is AM (and end is bigger than start), then use only today
// if start is PM and end is PM (and end is smaller than start), then use today and tomorrow
// - if date is between zero and the end, then use yesterday and today
// if start is PM and end is PM (and end is bigger than start), then use only today
// if start is PM and end is AM, then use today and tomorrow
// - if date is between zero and the end, then use yesterday and today
if ((Date_API::isAM($start)) && (Date_API::isPM($end))) {
$first = 0;
$second = 0;
}
if ((Date_API::isAM($start)) && (Date_API::isAM($end)) && ($end < $start)) {
if (($user->getHour() >= 0) && ($user->getHour() <= $end)) {
$first = -DAY;
$second = 0;
} else {
$first = 0;
$second = DAY;
}
}
if ((Date_API::isAM($start)) && (Date_API::isAM($end)) && ($end > $start)) {
$first = 0;
$second = 0;
}
if ((Date_API::isPM($start)) && (Date_API::isPM($end)) && ($end < $start)) {
if (($user->getHour() >= 0) && ($user->getHour() <= $end)) {
$first = -DAY;
$second = 0;
} else {
$first = 0;
$second = DAY;
}
}
if ((Date_API::isPM($start)) && (Date_API::isPM($end)) && ($end > $start)) {
$first = 0;
$second = 0;
}
if ((Date_API::isPM($start)) && (Date_API::isAM($end))) {
if (($user->getHour() >= 0) && ($user->getHour() <= $end)) {
$first = -DAY;
$second = 0;
} else {
$first = 0;
$second = DAY;
}
}
return array(
date('Y-m-d', $user->getDate(DATE_FORMAT_UNIXTIME) + $first),
date('Y-m-d', $user->getDate(DATE_FORMAT_UNIXTIME) + $second)
);
}
/**
* Retrieves the next assignee in the given project's round robin queue.
*
* @access public
* @param integer $prj_id The project ID
* @return integer The assignee's user ID
*/
function getNextAssignee($prj_id)
{
// get the full list of users for the given project
list($blackout_start, $blackout_end, $users) = Round_Robin::getUsersByProject($prj_id);
if (count($users) == 0) {
return 0;
} else {
$user_ids = array_keys($users);
$next_usr_id = 0;
foreach ($users as $usr_id => $details) {
if ($details['is_next']) {
$next_usr_id = $usr_id;
break;
}
}
// if no user is currently set as the 'next' assignee,
// then just get the first one in the list
if (empty($next_usr_id)) {
$next_usr_id = $user_ids[0];
}
// counter to keep the number of times we found an invalid user
$ignored_users = 0;
// check the blackout hours
do {
$user = new Date(Date_API::getCurrentUnixTimestampGMT());
$user->convertTZById($users[$next_usr_id]['timezone']);
list($today, $tomorrow) = Round_Robin::getBlackoutDates($user, $blackout_start, $blackout_end);
$first = new Date($today . ' ' . $blackout_start);
$first->setTZById($users[$next_usr_id]['timezone']);
$second = new Date($tomorrow . ' ' . $blackout_end);
$second->setTZById($users[$next_usr_id]['timezone']);
if ((Date::compare($first, $user) == -1) && (Date::compare($user, $second) == -1)) {
$ignored_users++;
$current_index = array_search($next_usr_id, $user_ids);
// if we reached the end of the list of users and none of them
// was a valid one, then just select the first one
// however, we want to complete at least one full iteration over the list of users
// that is, if we didn't start checking the users in the beginning of the list,
// then do another run over the users just in case
if (($ignored_users >= count($user_ids)) && ($current_index == (count($user_ids) - 1))) {
$assignee = $user_ids[0];
break;
}
// if we reached the end of the list, and we still didn't find an user,
// then go back to the beginning of the list one last time
if ($current_index == (count($user_ids) - 1)) {
$current_index = 0;
$next_usr_id = $user_ids[++$current_index];
$found = 0;
continue;
}
$next_usr_id = $user_ids[++$current_index];
$found = 0;
} else {
$assignee = $next_usr_id;
$found = 1;
}
} while (!$found);
// mark the next user in the list as the 'next' assignment
$assignee_index = array_search($assignee, $user_ids);
if ($assignee_index == (count($user_ids) -1)) {
$next_assignee = $user_ids[0];
} else {
$next_assignee = $user_ids[++$assignee_index];
}
Round_Robin::markNextAssignee($prj_id, $next_assignee);
return $assignee;
}
}
/**
* Marks the next user in the round robin list as the next assignee in the
* round robin queue.
*
* @access public
* @param integer $prj_id The project ID
* @param integer $usr_id The assignee's user ID
* @return boolean
*/
function markNextAssignee($prj_id, $usr_id)
{
$prr_id = Round_Robin::getID($prj_id);
$stmt = "UPDATE
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user
SET
rru_next=0
WHERE
rru_prr_id=$prr_id";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return false;
} else {
$stmt = "UPDATE
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user
SET
rru_next=1
WHERE
rru_usr_id=" . Misc::escapeInteger($usr_id) . " AND
rru_prr_id=$prr_id";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return false;
} else {
return true;
}
}
}
/**
* Returns the round robin entry ID associated with a given project.
*
* @access public
* @param integer $prj_id The project ID
* @return integer The round robin entry ID
*/
function getID($prj_id)
{
$stmt = "SELECT
prr_id
FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin
WHERE
prr_prj_id=" . Misc::escapeInteger($prj_id);
$res = $GLOBALS["db_api"]->dbh->getOne($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return "";
} else {
return $res;
}
}
/**
* Retrieves the list of users, round robin blackout hours and their
* respective preferences with regards to timezones.
*
* @access public
* @param integer $prj_id The project ID
* @return array The list of users
*/
function getUsersByProject($prj_id)
{
$stmt = "SELECT
usr_id,
usr_preferences,
rru_next,
prr_blackout_start,
prr_blackout_end
FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin,
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user,
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
WHERE
prr_prj_id=" . Misc::escapeInteger($prj_id) . " AND
prr_id=rru_prr_id AND
rru_usr_id=usr_id
ORDER BY
usr_id ASC";
$res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return array();
} else {
$blackout_start = '';
$blackout_end = '';
$t = array();
for ($i = 0; $i < count($res); $i++) {
$blackout_start = $res[$i]['prr_blackout_start'];
$blackout_end = $res[$i]['prr_blackout_end'];
$prefs = unserialize($res[$i]['usr_preferences']);
$t[$res[$i]['usr_id']] = array(
'timezone' => $prefs['timezone'],
'is_next' => $res[$i]['rru_next']
);
}
return array(
$blackout_start,
$blackout_end,
$t
);
}
}
/**
* Creates a new round robin entry.
*
* @access public
* @return integer 1 if the creation worked, -1 otherwise
*/
function insert()
{
$blackout_start = $_POST['blackout_start']['Hour'] . ':' . $_POST['blackout_start']['Minute'] . ':00';
$blackout_end = $_POST['blackout_end']['Hour'] . ':' . $_POST['blackout_end']['Minute'] . ':00';
$stmt = "INSERT INTO
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin
(
prr_prj_id,
prr_blackout_start,
prr_blackout_end
) VALUES (
" . Misc::escapeInteger($_POST["project"]) . ",
'" . Misc::escapeString($blackout_start) . "',
'" . Misc::escapeString($blackout_end) . "'
)";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return -1;
} else {
$new_id = $GLOBALS["db_api"]->get_last_insert_id();
// add all of the user associated with this round robin entry
foreach ($_POST['users'] as $usr_id) {
Round_Robin::addUserAssociation($new_id, $usr_id);
}
return 1;
}
}
/**
* Associates a round robin entry with a user ID.
*
* @access public
* @param integer $prr_id The round robin entry ID
* @param integer $usr_id The user ID
* @return boolean
*/
function addUserAssociation($prr_id, $usr_id)
{
$stmt = "INSERT INTO
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user
(
rru_prr_id,
rru_usr_id,
rru_next
) VALUES (
" . Misc::escapeInteger($prr_id) . ",
" . Misc::escapeInteger($usr_id) . ",
0
)";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return false;
} else {
return true;
}
}
/**
* Method used to get the list of round robin entries available in the
* system.
*
* @access public
* @return array The list of round robin entries
*/
function getList()
{
$stmt = "SELECT
prr_id,
prj_title
FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin,
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
WHERE
prr_prj_id=prj_id
ORDER BY
prj_title ASC";
$res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return "";
} else {
// get the list of associated users
for ($i = 0; $i < count($res); $i++) {
$res[$i]['users'] = implode(", ", array_values(Round_Robin::getAssociatedUsers($res[$i]['prr_id'])));
}
return $res;
}
}
/**
* Returns an associative array in the form of user id => name of the users
* associated to a given round robin entry ID.
*
* @access public
* @param integer $prr_id The round robin entry ID
* @return array The list of users
*/
function getAssociatedUsers($prr_id)
{
$stmt = "SELECT
usr_id,
usr_full_name
FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user,
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
WHERE
rru_usr_id=usr_id AND
rru_prr_id=" . Misc::escapeInteger($prr_id) . "
ORDER BY
usr_id ASC";
$res = $GLOBALS["db_api"]->dbh->getAssoc($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return array();
} else {
return $res;
}
}
/**
* Method used to get the details of a round robin entry.
*
* @access public
* @param integer $prr_id The round robin entry ID
* @return array The round robin entry details
*/
function getDetails($prr_id)
{
$stmt = "SELECT
*
FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin
WHERE
prr_id=" . Misc::escapeInteger($prr_id);
$res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return "";
} else {
// get all of the user associations here as well
$res['users'] = array_keys(Round_Robin::getAssociatedUsers($res['prr_id']));
return $res;
}
}
/**
* Method used to update a round robin entry in the system.
*
* @access public
* @return integer 1 if the update worked, -1 otherwise
*/
function update()
{
$blackout_start = $_POST['blackout_start']['Hour'] . ':' . $_POST['blackout_start']['Minute'] . ':00';
$blackout_end = $_POST['blackout_end']['Hour'] . ':' . $_POST['blackout_end']['Minute'] . ':00';
$stmt = "UPDATE
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin
SET
prr_prj_id=" . Misc::escapeInteger($_POST["project"]) . ",
prr_blackout_start='" . Misc::escapeString($blackout_start) . "',
prr_blackout_end='" . Misc::escapeString($blackout_end) . "'
WHERE
prr_id=" . Misc::escapeInteger($_POST["id"]);
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return -1;
} else {
// remove all of the associations with users, then add them all again
Round_Robin::removeUserAssociations($_POST['id']);
foreach ($_POST['users'] as $usr_id) {
Round_Robin::addUserAssociation($_POST['id'], $usr_id);
}
return 1;
}
}
/**
* Method used to remove the user associations for a given round robin
* entry ID.
*
* @access public
* @param integer $prr_id The round robin ID
* @return boolean
*/
function removeUserAssociations($prr_id)
{
if (!is_array($prr_id)) {
$prr_id = array($prr_id);
}
$items = @implode(", ", Misc::escapeInteger($prr_id));
$stmt = "DELETE FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "round_robin_user
WHERE
rru_prr_id IN ($items)";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return false;
} else {
return true;
}
}
/**
* Method used to remove a round robin entry from the system.
*
* @access public
* @return boolean
*/
function remove()
{
$items = @implode(", ", Misc::escapeInteger($_POST["items"]));
$stmt = "DELETE FROM
" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_round_robin
WHERE
prr_id IN ($items)";
$res = $GLOBALS["db_api"]->dbh->query($stmt);
if (PEAR::isError($res)) {
Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
return false;
} else {
Round_Robin::removeUserAssociations($_POST['items']);
return true;
}
}
}
// benchmarking the included file (aka setup time)
if (APP_BENCHMARK) {
$GLOBALS['bench']->setMarker('Included Round_Robin Class');
}