';
return $output;
}
}
/**
* Implementation of hook_init().
*/
function issue_tracker_init() {
// AJAX callback to get an issue's status
drupal_add_js('Drupal.behaviors.issue_tracker = function(context) {
$(\'a[class=extiw issue]\').each(function() {
// extract issue number from the URL
var issueURL = $(this)[0].href;
var issueURLSplitted = issueURL.split(\'/\')
var issueNumber = issueURLSplitted[issueURLSplitted.length - 1];
var target= $(this);
$.get(Drupal.settings.basePath + "ajax/issue_get_status/" + issueNumber, null, function(response) {
var result = Drupal.parseJson(response);
var decorationString = "";
if (result.issueClosedStatus == true) {
decorationString = \'line-through\';
}
$(target).css(\'text-decoration\', decorationString);
});
});
}', 'inline');
}
/**
* Implementation of hook_form().
* This form is displayed when creating new issues or editing an issue (not when replying to an issue)
*/
function issue_tracker_form(&$node) {
global $nid;
$iid = $_GET['iid'];
$type = node_get_types('type', $node);
$form['title'] = array(
'#type' => 'textfield',
'#title' => check_plain($type->title_label),
'#required' => TRUE,
'#default_value' => $node->title,
'#weight' => -5
);
$form['body_filter']['body'] = array(
'#type' => 'textarea',
'#title' => check_plain($type->body_label),
'#default_value' => $node->body,
'#rows' => 20,
'#required' => False
);
$form['body_filter']['filter'] = filter_form($node->format);
$form['#submit'] = array('issue_tracker_form_submit');
return $form;
}
/**
* Implementation of hook_menu().
*/
function issue_tracker_menu() {
$items = array();
$items['issues'] = array(
'title' => 'Issues',
'page callback' => 'issues_page',
'access callback' => 'user_access',
'access arguments' => array('access content'),
);
// Issue Filter redirect
// filtername for redirection is fetched by loader-function and passed to drupal_goto
$items['issues/filter/%filtername'] = array(
'title' => 'Issues Filter Bookmark',
'page callback' => 'issues_page',
'page arguments' => array(2),
'access callback' => 'user_access',
'access arguments' => array('access content'),
'load arguments' => array('%map', '%index'),
'type' => MENU_CALLBACK
);
// Issue details redirect
// id for redirection is fetched by loader-function and passed to drupal_goto
$items['issues/%issue'] = array(
'title' => 'Issues',
'page callback' => 'drupal_goto',
'page arguments' => array(1),
'access callback' => 'user_access',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK
);
//The ajax way for getting issues
$items['ajax/get_issues'] = array(
'page callback' => 'issue_get_data',
'access callback' => 'user_access',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK
);
//Getting filters via ajax
$items['ajax/issue_filters'] = array(
'page callback' => 'issue_filter_action',
'access callback' => 'user_access',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK
);
$items['ajax/issue_get_status/%'] = array(
'page callback' => 'issue_tracker_get_closed_status',
'page arguments' => array(2),
'access callback' => 'user_access',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK
);
// Add 'delete' tab
$items['node/%node/delete_issue'] = array(
'title' => t('Delete'),
'page callback' => 'drupal_get_form',
'page arguments' => array('node_delete_confirm', 1),
'access callback' => '_issue_tracker_delete_tab_access',
'access arguments' => array(1),
'weight' => 2,
'type' => MENU_LOCAL_TASK,
'file' => 'node.pages.inc',
'file path' => drupal_get_path('module', 'node')
);
return $items;
}
/**
* Implementation of hook_theme().
*/
function issue_tracker_theme() {
return array(
'issue_tracker_pager' => array(
'arguments' => array('limit' => 25, 'element' => 0)
),
'issue_tracker_pager_first' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0)
),
'issue_tracker_pager_previous' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'interval' => 1, 'parameters' => array())
),
'issue_tracker_pager_next' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0, 'interval' => 1)
),
'issue_tracker_pager_last' => array(
'arguments' => array('text' => NULL, 'limit' => NULL, 'element' => 0)
),
'issue_tracker_pager_list' => array(
'arguments' => array('limit' => NULL, 'element' => 0, 'quantity' => 5, 'text' => '')
),
'issue_tracker_pager_link' => array(
'arguments' => array('text' => NULL, 'target_page' => NULL, 'element' => NULL, 'attributes' => array())
),
'issue_tracker_filter' => array(
'arguments' => array('filters' => NULL, 'id' => NULL),
),
'issue_tracker_hidden_filter' => array(
'arguments' => array('filters' => NULL, 'id' => NULL),
),
'issue_tracker_free_filter' => array(
'arguments' => array('filters' => NULL),
),
);
}
/**
* Menu callback for path 'issues/$id'.
* Returns the path and node-ID for issue `$id' to pass to drupal_goto().
*
* @param $issue_id of the issue
* @return node-ID of the requested issue.
*/
function issue_load($issue_id) {
if (!is_numeric($issue_id)) {
return FALSE;
}
$issue_query = db_query("SELECT nid FROM {issues} WHERE issue_id = %d", $issue_id);
$issue = db_fetch_object($issue_query);
return 'node/'. $issue->nid;
}
/**
* Menu callback for path 'issues/filter/$filtername'.
* Returns the path and filtername for filter `$fid'.
*
* @param $filter_id of the filter
* @return path of the requested filter.
*/
function filtername_load($filtername, $map_array, $index) {
global $user;
if (!is_string($filtername)) {
return FALSE;
}
foreach ($map_array as $i => $value) {
if ($i == 2) {
$fullfilter = $map_array[$i];
}
if ($i > 2) {
$fullfilter .= '/' . $map_array[$i];
}
}
$filter_query = db_query("SELECT fid, name, url FROM {issue_filter} f WHERE f.uid=%d AND name = LOWER('%s')", $user->uid, $fullfilter);
$filter = db_fetch_object($filter_query);
return $filter;
}
/**
* Implementation of hook_link().
*/
function issue_tracker_link($type, $node = 0, $main = 0) {
$links = array();
switch ($type) {
case 'node':
if ($node->type == 'issue') {
if (user_access('create issue')) {
$links['create issue'] = array(
'title' => t('Post Reply'),
'href' => "comment/reply/$node->nid",
'fragment' => 'comment-form',
);
}
}
break;
}
return $links;
}
/**
* Implementation of hook_link_alter().
*/
function issue_tracker_link_alter(&$links, $node) {
if ($node->type == 'issue' && isset($links['comment_add'])) {
$links['comment_add']['title'] = "Post Reply";
$links['comment_add']['attributes']['title'] = "Reply to this issue.";
}
}
/**
* Implementation of hook_token_list().
* Defines a custom token type for issues, which can be used by the path (and
* pathauto) module to generate url-aliases for issues referencing their IDs.
*
* @param type
* A flag indicating the class of substitution tokens to return
* information on. If this is set to 'all', a complete list is being
* built and your module should return its full list, regardless of
* type. Global tokens should always be returned, regardless of the
* $type passed in.
* @return
* A keyed array listing the substitution tokens. Elements should be
* in the form of: $list[$type][$token] = $description
*/
function issue_tracker_token_list($type = 'all') {
if ($type == 'node') {
$tokens['node']['issue-id'] = t('The Origo issue-ID of the issue-node.');
};
return $tokens;
};
/**
* Implementation of hook_token_values().
* Returns the Origo issue-ID for replacing [issue-id] tokens.
*
* @param type
* A flag indicating the class of substitution tokens to use. If an
* object is passed in the second param, 'type' should contain the
* object's type. For example, 'node', 'comment', or 'user'. If your
* implemention of the hook inserts globally applicable tokens that
* do not depend on a particular object, it should only return values
* when $type is 'global'.
* @param object
* Optionally, the object to use for building substitution values.
* A node, comment, user, etc.
* @return
* A keyed array containing the substitution tokens and the substition
* values for the passed-in type and object.
*/
function issue_tracker_token_values($type, $object = NULL, $options = array()) {
$values = array();
switch ($type) {
case 'node' :
$node = $object;
// TODO: is there a better way to check if $node is an issue-node?
// bherlig, 2010-01-19
if (isset($node->issue_id)) {
$values['issue-id'] = $node->issue_id;
}
break;
};
return $values;
};
/**
* Implementation of hook_view().
* Displays the issue content together with the issue replies which have customized rendering.
* Replying to an issue invokes this _view hook with $page = FALSE
*/
function issue_tracker_view($node, $teaser = FALSE, $page = FALSE) {
global $user;
//prevent comments from being displayed
if ($node->type == 'issue') {
// add issue-header
$node->issue_header = _issue_tracker_build_issue_header();
if ($page) {
// Breadcrumb navigation
$breadcrumb[] = l(t('Home'), NULL);
$breadcrumb[] = l(t('issues'), 'issues');
drupal_set_breadcrumb($breadcrumb);
}
$issue_query = db_query("SELECT issue_id FROM {issues} WHERE nid = %d", $node->nid);
$issue = db_fetch_object($issue_query);
if ($page) {
drupal_set_title('#'. $issue->issue_id .' - '. htmlentities(utf8_decode($node->title)));
}
else {
$node->title = '#'. $issue->issue_id .' - '. utf8_decode($node->title);
}
$node->comment = 0;
$node->content['issue_comments'] = array(
'#value' => issue_comment_view($node),
'#weight' => 2,
);
$planning_text = '';
if (isset($user->uid) && $user->uid > 0) {
$issue_data = origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.retrieve_planning_data', (int)variable_get('origo_project_id', 0), (int)$issue->issue_id);
if ($issue_data['deadline'] > 0) { // suppress timestamp 0
$planning_text .= t('Deadline: ') . gmdate('Y-m-d', (int)$issue_data['deadline']);
}
if ($issue_data['work_amount'] > 0) {
$planning_text .= ' '. t('Estimated amount of work: ') . $issue_data['work_amount'] . format_plural($issue_data['work_amount'], ' day', ' days');
}
}
$node->content['planning'] = array(
'#value' => $planning_text,
'#weight' => -2
);
if (isset($user->uid) && $user->uid > 0) {
$terms = taxonomy_node_get_terms($node);
$tags = array();
foreach ($terms as $term) {
$tags[] = $term->name;
}
$is_subscribed = FALSE;
foreach ($tags as $tag) {
if ($tag == 'subscribed::'. $user->name) {
$is_subscribed = TRUE;
}
}
$node->content['subscription'] = array(
'#value' => drupal_get_form('issue_subscription_form', $node->nid, $is_subscribed, implode(', ', $tags)),
'#weight' => -1
);
}
}
return node_prepare($node, $teaser);
}
/**
* Implementation of hook_validate().
* We misuse the validation hook to insert an issue into the backend. This way,
* we can abort creating a Drupal node in case the back-end failed creating the
* node there (quite possibly because it crashed and is not available).
* This should fix #675 for good (or at least until there's a better way).
*/
function issue_tracker_form_validate($form, &$form_state) {
// If the node already exists, we're not interested in it. Return immediately.
if (isset($form['#node']) && isset($form['#node']->nid)) {
return True;
}
// Trim title
$form_state['values']['title'] = trim($form_state['values']['title']);
// Only execute if the form was really submitted - but not if it's just a preview.
// TODO: is there a better way to decide if it's a preview or a submission?
// bherlig, 2010-06-16
if ($form_state['clicked_button']['#id'] == 'edit-submit') {
// the user submitted the form -> try to insert the issue into the back-end.
$vid = _issue_tracker_get_issue_vocabulary_id();
if (user_access('edit issue')) {
// The user may have defined "status|assigned|resolution" tags,
// we thus strip them before proceeding.
// Further, we enforce unique tags, i.e. no tag can appear more than once per node
$tags = $form_state['values']['taxonomy']['tags'][$vid];
$tag_seq = explode(",", $tags);
$tag_seq = array_map('trim', $tag_seq); // trim all entries.
$tags_unique = array_unique($tag_seq); // retain unique entries
// we now have a list of unique, trimmed strings (i.e. tags)
$cleaned_tag_seq = array();
foreach ($tags_unique as $tag) {
if ((strpos($tag, "status::") === 0) || (strpos($tag, "assigned::") === 0) || (strpos($tag, "resolution::") === 0)) {
// it's a special tag: throw it away!
}
else {
$cleaned_tag_seq[] = $tag;
}
}
$form_state['values']['taxonomy']['tags'][$vid] = implode(",", $cleaned_tag_seq);
if ($form_state['values']['resolution'] != '-') {
$form_state['values']['issuestatus'] = 'status::closed';
}
if ($form_state['values']['taxonomy']['tags'][$vid] == '') {
$form_state['values']['taxonomy']['tags'][$vid] = $form_state['values']['issuestatus'];
}
else {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['issuestatus'];
}
if ($form_state['values']['assignedto'] != '-') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['assignedto'];
}
if ($form_state['values']['resolution'] != '-') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['resolution'];
}
if ($form_state['values']['subscription'] != '') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['subscription'];
}
}
else {
$form_state['values']['taxonomy']['tags'][$vid] = 'status::open';
}
// this was in node_insert()
$deadline = $form_state['values']['deadline'];
if ($deadline != NULL) {
$deadline_timestamp = gmmktime(0, 0, 0, $deadline['month'], $deadline['day'], $deadline['year']);
}
else {
$deadline_timestamp = (int)0;
}
//add issue into backend
// call is coming from the web interface -> issue has to be created in the backend
$project_id = (int)variable_get('origo_project_id', 0);
$issue_id = origo_auth_xmlrpc_session(
variable_get('origo_api_internal', ''),
'internal_issue.add_2',
(int)$project_id,
$form_state['values']['title'],
$form_state['values']['body'],
$form_state['values']['taxonomy']['tags'][$vid],
(bool)$form_state['values']['private'],
(int)$deadline_timestamp,
(int)$form_state['values']['work_amount']);
if ($issue_id == 0) {
// insertion in the backend failed
// set message & abort
$nb = xmlrpc_errno();
$xml_msg = xmlrpc_error_msg();
$msg = "Oops, insertion into the back-end failed - a node might be unavailable. Received error message: \"$xml_msg\" (Errorno $nb) ";
$msg .= "Try waiting a few minutes and submit the issue again.";
form_set_error('title', $msg);
}
else {
// We store the issue-id into the form-values, so it get's baked into the node as field in its object.
// We will retrieve & process it later on in hook_insert().
$form_state['values']['issue_id'] = $issue_id;
}
}
}
/**
* submit function for issues (form: 'issue_node_form')
*/
function issue_tracker_form_submit($form, &$form_state) {
// The node must alread exist (i.e. it's being edited)
if (isset($form['#node']) && isset($form['#node']->nid)) {
// need edit permissions
if (user_access('edit issue')) {
$vid = _issue_tracker_get_issue_vocabulary_id();
// The user may have defined "status|assigned|resolution" tags,
// we thus strip them before proceeding.
// Further, we enforce unique tags, i.e. no tag can appear more than once per node
$tags = $form_state['values']['taxonomy']['tags'][$vid];
$tag_seq = explode(",", $tags);
$tag_seq = array_map('trim', $tag_seq); // trim all entries.
$tags_unique = array_unique($tag_seq); // retain unique entries
// we now have a list of unique, trimmed strings (i.e. tags)
$cleaned_tag_seq = array();
foreach ($tags_unique as $tag) {
if ((strpos($tag, "status::") === 0) || (strpos($tag, "assigned::") === 0) || (strpos($tag, "resolution::") === 0)) {
// it's a special tag: throw it away!
}
else {
$cleaned_tag_seq[] = $tag;
}
}
$form_state['values']['taxonomy']['tags'][$vid] = implode(",", $cleaned_tag_seq);
if ($form_state['values']['resolution'] != '-') {
$form_state['values']['issuestatus'] = 'status::closed';
}
if ($form_state['values']['taxonomy']['tags'][$vid] == '') {
$form_state['values']['taxonomy']['tags'][$vid] = $form_state['values']['issuestatus'];
}
else {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['issuestatus'];
}
if ($form_state['values']['assignedto'] != '-') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['assignedto'];
}
if ($form_state['values']['resolution'] != '-') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['resolution'];
}
if ($form_state['values']['subscription'] != '') {
$form_state['values']['taxonomy']['tags'][$vid] .= ', '. $form_state['values']['subscription'];
}
}
else {
$form_state['values']['taxonomy']['tags'][$vid] = 'status::open';
}
}
}
/**
* Implementation of hook_form_alter().
* We use this function only for hiding the taxonomy-field for non-project members. This has to be
* done from here, as hook_form_FORM_ID_alter is called before taxonomy_form_alter will write its fields.
*/
function issue_tracker_form_alter(&$form, &$form_state) {
if (!user_access('edit issue')) {
$form['taxonomy']['tags'][_issue_tracker_get_issue_vocabulary_id()]['#type'] = 'hidden';
}
}
/**
* Implementation of hook_form_FORM_ID_alter().
* @param $form
* @param $form_state
* @return unknown_type
*/
function issue_tracker_form_issue_node_form_alter(&$form, &$form_state) {
$issue_body = '';
$issue_status = '';
$issuestatus_default = 'status::open';
$issue_assigned_to = '-';
$issue_resolution = '-';
$default_work_amount = 0;
// set the default deadline to one week after today, 604800 = 7 * 24 * 60 * 60
$default_deadline = gmgetdate(time() + 604800);
if ($form_state['values']['op'] == 'Preview') {
// read previously submitted values
$issue_body = $form_state['values']['body'];
$issue_status = $form_state['values']['issuestatus'];
$issue_assigned_to = $form_state['values']['assignedto'];
$issue_resolution = $form_state['values']['resolution'];
$default_work_amount = $form_state['values']['work_amount'];
$default_deadline = array(
'year' => $form_state['values']['deadline']['year'],
'mon' => $form_state['values']['deadline']['month'],
'mday' => $form_state['values']['deadline']['day'],
);
}
else {
$issue_body = "'''What steps will reproduce the problem?'''\r\n" .
"* 1.\r\n" .
"* 2.\r\n" .
"* 3.\r\n" .
"\r\n" .
"'''What is the expected output? What do you see instead?'''\r\n" .
"\r\n" .
"\r\n" .
"'''What version of the product are you using? On what operating system?'''\r\n" .
"\r\n" .
"\r\n" .
"'''Please provide any additional information below:'''\r\n";
$issue_status = $issuestatus_default;
}
drupal_add_js('document.getElementById(\'edit-title\').focus()', 'inline', 'footer');
if ($form['nid']['#value']=='') {
$form['body_filter']['body']['#default_value'] = $issue_body;
}
$form['#validate'][] = 'issue_tracker_form_validate';
if (user_access('edit issue')) {
// include JavaScript for the "take" button
global $user;
drupal_add_js('var drupal_username = "'. $user->name .'";', 'inline');
drupal_add_js(drupal_get_path('module', 'issue_tracker') .'/js/edit_issue.js');
//Set weights
$form['taxonomy']['#weight'] = 4;
$form['body_filter']['#weight'] = 7;
$form['title']['#weight'] = -5;
$form['private']['#weight'] = 8;
// Issue Status
$form['issuestatus'] = array(
'#prefix' => "
",
'#type' => 'select',
'#title' => t('Status'),
'#options' => array('status::open' => t('Open'), 'status::closed' => t('Closed')),
'#weight' => 2,
'#default_value' => $issue_status,
);
// Assigned to:
// TODO: this is called also when previewing - and there we shouldn't have to load the values from the back-end.
// Also, if the back-end's down, this will produce an "array merge" errormessage:
//
# warning: array_merge() [function.array-merge]: Argument #1 is not an array in /var/www/drupal/sites/all/modules/issue_tracker/issue_tracker.module on line 657.
# warning: array_merge() [function.array-merge]: Argument #2 is not an array in /var/www/drupal/sites/all/modules/issue_tracker/issue_tracker.module on line 657.
# warning: Invalid argument supplied for foreach() in /var/www/drupal/sites/all/modules/issue_tracker/issue_tracker.module on line 660.
// bherlig, 2010-06-17
$members1 = origo_auth_xmlrpc_session(variable_get('origo_api', ''), 'project.members', variable_get('origo_project_id', 0), 3);
$members2 = origo_auth_xmlrpc_session(variable_get('origo_api', ''), 'project.members', variable_get('origo_project_id', 0), 4);
$members = array_merge($members1, $members2);
$users = array('-' => t('-'));
foreach ($members as $member) {
$users['assigned::'. $member['name']] = t($member['name']);
}
asort($users);
$form['assignedto'] = array(
'#type' => 'select',
'#title' => t('Assigned to'),
'#options' => $users,
'#weight' => 3,
'#default_value' => $issue_assigned_to,
);
$form['resolution'] = array(
'#type' => 'select',
'#title' => t('Resolution'),
'#options' => array(
'-' => t('-'),
'resolution::fixed' => t('Fixed'),
'resolution::wontfix' => t('Won\'t fix'),
'resolution::duplicate' => t('Duplicate'),
'resolution::worksforme' => t('Works for me')
),
'#weight' => 4,
'#default_value' => $issue_resolution,
);
// load values from the backend, but only if it's not a preview (because the values might have been changed in the meantime)
if ($form['nid']['#value'] != '' && $form_state['values']['op'] != 'Preview') {
$nid = $form['nid']['#value'];
$issue_query = db_query("SELECT issue_id FROM {issues} WHERE nid = %d", $nid);
$issue = db_fetch_object($issue_query);
$issue_data = origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.retrieve_planning_data', (int)variable_get('origo_project_id', 0), (int)$issue->issue_id);
// get deadline from backend, if existing. otherwise, the default deadline as calculated above will be used.
if ($issue_data['deadline'] != 0) {
$default_deadline = gmgetdate($issue_data['deadline']);
}
$default_work_amount = $issue_data['work_amount'];
}
$form['deadline'] = array(
'#type' => 'date',
'#title' => t('Deadline'),
'#description' => t('The date until the issue should be fixed'),
'#default_value' => array('year' => $default_deadline['year'], 'month' => $default_deadline['mon'], 'day' => $default_deadline['mday']),
'#weight' => 5,
);
$work_amount_options = array(0 => '-');
for ($i = 1; $i <= 14; $i++) {
$work_amount_options[$i] = $i;
}
$form['work_amount'] = array(
'#type' => 'select',
'#title' => t('Work amount'),
'#options' => $work_amount_options,
'#default_value' => $default_work_amount,
'#description' => t('Estimated number of days needed to fix the issue'),
'#weight' => 6,
'#suffix' => "
", // end 'issue-edit'
);
if ($form['nid']['#value'] != '' || $form_state['values']['op'] == 'Preview') {
$vid = _issue_tracker_get_issue_vocabulary_id();
$subscription_tags = array();
foreach ($form['#node']->taxonomy as $id => $obj) {
$tag = '';
if (is_array($obj)) {
// when previewing, $obj is an array
$tag = $obj[(String)$vid];
}
else {
// when editing an existing node, $obj is an stdClass object
$tag = $obj->name;
}
if (strpos($tag, "status") === 0) {
$form['issuestatus']['#default_value'] = $tag;
unset($form['#node']->taxonomy[$id]);
}
elseif (strpos($tag, "assigned") === 0) {
$form['assignedto']['#default_value'] = $tag;
unset($form['#node']->taxonomy[$id]);
}
elseif (strpos($tag, "resolution") === 0) {
$form['resolution']['#default_value'] = $tag;
unset($form['#node']->taxonomy[$id]);
}
elseif (strpos($tag, "subscribed") === 0) {
$subscription_tags[] = $tag;
}
}
// replacement will be done in order of items in array
// this order may NOT be the same as the numerical order of the keys
$pattern[0] = '/(assigned|status|subscribed|resolution)::[a-z_]+(, )/';
$pattern[1] = '/(, )(assigned|status|subscribed|resolution)::[a-z_]+$/';
$pattern[2] = '/(assigned|status|subscribed|resolution)::[a-z_]+/';
$replacement[0] = '';
$replacement[1] = '';
$replacement[2] = '';
$form['taxonomy']['tags'][$vid]= preg_replace($pattern, $replacement, $form['taxonomy']['tags'][$vid]);
$form['subscription'] = array(
'#type' => 'hidden',
'#value' => implode(', ', $subscription_tags)
);
}
}
}
/**
* Implementation of hook_form_FORM_ID_alter().
* Overrides the node module's deletion confirmation form.
*/
function issue_tracker_form_node_delete_confirm_alter(&$form, &$form_state) {
$node = node_load($form['nid']['#value']);
if (_issue_tracker_delete_tab_enabled($node->type)) {
drupal_set_title(t('Are you sure you want to delete issue "%title"?', array('%title' => $node->title)));
$form['log'] = array(
'#type' => 'textfield',
'#title' => t('Log message'),
'#default_value' => t('Deleted'),
'#weight' => -1,
);
$form['description']['#value'] = t('The issue and all related data (e.g. workitems) will be permanently deleted. This action cannot be undone.');
$form['actions']['submit']['#value'] = t('Delete');
unset($form['#submit']);
$form['#submit'] = array('issue_tracker_node_delete_confirm_submit');
}
}
/**
* Submit handler for the 'node_delete_confirm' form of the node module.
* This is called instead of the original submit handler for issue nodes.
*/
function issue_tracker_node_delete_confirm_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
$node = node_load($form_state['values']['nid']);
if (_issue_tracker_delete_tab_enabled($node->type)) {
$node->log = $form_state['values']['log'];
// Send call to backend
$issue_id = _issue_id_from_nid($node->nid);
origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.delete', (int)variable_get('origo_project_id', 0), (int)$issue_id);
$nb = xmlrpc_errno();
$msg = xmlrpc_error_msg();
if (isset($nb)) {
drupal_set_message('Error deleting issue: '. $msg .' (Errorno '. $nb .')');
}
else {
// delete the node
node_delete($form_state['values']['nid']);
}
// redirect to issue overview
$form_state['redirect'] = 'issues';
}
else {
// Call original submit handler
node_delete_confirm_submit($form, $form_state);
}
}
}
/**
* Implementation of hook_update().
* Called upon updating an issue.
*/
function issue_tracker_update($node) {
if (! isset($node->source) || $node->source != "api") {
// call is coming from the web (frontend)
$title = $node->title;
$description = $node->body;
$tags = $node->taxonomy['tags'][_issue_tracker_get_issue_vocabulary_id()];
$private = (bool)$node->private;
$deadline = $node->deadline;
$deadline_timestamp = gmmktime(0, 0, 0, $deadline['month'], $deadline['day'], $deadline['year']);
$work_amount = (int)$node->work_amount;
$issue_id = 0;
$result = db_query("SELECT issue_id FROM {issues} WHERE nid=%d", $node->nid);
$issue_id = db_fetch_array($result);
// Update issue
origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.update_2', (int)variable_get('origo_project_id', 0), (int)$issue_id['issue_id'], $title, $description, $tags, (bool)$private, $deadline_timestamp, $work_amount);
if ((int)xmlrpc_errno() > 0) {
$msg = t('Failed to update the issue in the back-end, response was: ');
$msg .= t(xmlrpc_error_msg());
$msg .= ' (Error #'. xmlrpc_errno() .')';
drupal_set_message($msg, 'warning');
}
$node->issue_id = $issue_id['issue_id'];
}
}
/**
* Implementation of hook_insert().
*/
function issue_tracker_insert($node) {
db_query("INSERT INTO {issues} (nid, issue_id) VALUES (%d, %d)", $node->nid, $node->issue_id);
}
/**
* Implementation of hook_delete().
* Removes issue-data from issue-tracker specific DB tables.
*/
function issue_tracker_delete($node) {
db_query("DELETE FROM {issues} WHERE nid = %d", $node->nid);
}
function issue_tracker_nodeapi(&$node, $op, $teaser) {
global $_SESSION;
switch ($op) {
case 'insert':
if (isset($node->files)) {
$issue_id = $node->issue_id;
foreach ($node->files as $fid => $file) {
$file = (object)$file;
$pos = strrpos($file->filepath, '/');
$filename = substr($file->filepath, $pos + 1);
$attachment_id = origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.add_attachment', (int)variable_get('origo_project_id', 0), (int)$issue_id, $filename, "(no description)");
}
}
break;
case 'update':
if (isset($node->files)) {
$issue_id = $node->issue_id;
$attachments = origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.retrieve_attachments', (int)variable_get('origo_project_id', 0), (int)$issue_id);
// add missing attachments and remove needless attachments
if (isset($node->files) && sizeof($node->files) > 0) {
foreach ($node->files as $fid => $file) {
$file = (object)$file;
$pos = strrpos($file->filepath, '/');
$filename = substr($file->filepath, $pos + 1);
if (!attachments_contain_file($attachments, $filename)) {
//the file is not in the list of attachments saved in the origo db
origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.add_attachment', (int)variable_get('origo_project_id', 0), (int)$issue_id, $filename, "(no description)");
}
elseif ($file->remove) {
//the file has to be removed
origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.remove_attachment', (int)variable_get('origo_project_id', 0), (int)$issue_id, $filename);
}
}
}
elseif (($attachments != FALSE) && sizeof($attachments) > 0) {
// there are entries in $attachments, but $node does not have them anymore
// -> they were all removed
foreach ($attachments as $a) {
origo_auth_xmlrpc_session(variable_get('origo_api_internal', ''), 'internal_issue.remove_attachment', (int)variable_get('origo_project_id', 0), (int)$issue_id, $a['file_name']);
}
}
}
break;
}
}
/**
* Menu callback for path 'issues' (issues page)
*/
function issues_page($filter = NULL) {
global $user;
// load the issues-per-page setting via origo_home
module_load_include('inc', 'origo_home', 'origo_constants');
module_load_include('inc', 'origo_home', 'origo_settings');
$post_per_page = get_origo_setting(ORIGO_ISSUE_AMOUNT, 25, $user->name);
drupal_add_css(drupal_get_path('module', 'issue_tracker') .'/issue_tracker.css');
// load js for base64 en/decoding for configuring filters or personalized filter-buttons
// This is only needed for members/owners/admins
if(user_access('edit issue')) {
load_webtoolkit_base64();
}
$stats = build_issue_stats_bar();
$data = issue_data_provider();
$header = $data['header'];
$rows = $data['rows'];
$output .= _issue_tracker_build_issue_header();
$output .= theme('table', array(), $stats, array('style' => 'width:100%'));
$output .= '
'. l('Create new issue report', 'node/add/issue') .'
';
return $output;
}
/**
* Utility function for calculating data for the "open/closed" bar on top of the main issue tracker page.
*/
function build_issue_stats_bar() {
$result = db_query('
SELECT t.name, COUNT(*) as count FROM
(SELECT tid, name FROM {term_data} WHERE name LIKE "status::%") t
JOIN
{term_node} tn ON t.tid = tn.tid
JOIN
{issues} i ON tn.nid = i.nid
GROUP BY t.name
ORDER BY t.name ASC');
$open = 0;
$closed = 0;
$undefined = 0;
while ($obj = db_fetch_object($result)) {
switch ($obj->name) {
case "status::open":
$open = $obj->count;
break;
case "status::closed":
$closed = $obj->count;
break;
default:
$undefined = $obj->count;
}
}
$total = ($open + $closed + $undefined)/100;
$statrow = array();
if ($open>0) {
$statrow[] = array('data' => t("open ($open)"), 'style' => 'background-color:#FFBBBB; white-space:nowrap; width:'. $open/$total .'%');
}
if ($undefined>0) {
$statrow[] = array('data' => t("undefined ($undefined)"), 'style' => 'background-color:#DDDDDD; white-space:nowrap; width:'. $undefined/$total .'%');
}
if ($closed>0) {
$statrow[] = array('data' => t("closed ($closed)"), 'style' => 'background-color:#BBFFBB; white-space:nowrap; width:'. $closed/$total .'%');
}
return array($statrow);
}
function build_filter() {
$output = "";
// drag-n-drop only for owners
if(user_access('origo project settings')) {
jquery_ui_add(array('ui.sortable', 'ui.draggable', 'ui.droppable'));
drupal_add_js(drupal_get_path('module', 'issue_tracker') .'/js/edit_filters.js');
}
$filter_order = variable_get('issue_tracker_filter_order', NULL);
$tags = retrieve_tags($filter_order);
// prepare static filters
$filters = array();
$filters = array_merge($filters, issue_tracker_status_filter());
$filters += issue_tracker_resolution_filter();
$filters += issue_tracker_visibility_filter();
$filters += issue_tracker_reporter_filter();
// Check if any of the special filters above are currently hidden,
// and add their tags to the general filter box, if necessary
$hidden_filters = array();
if (isset($filter_order) && is_array($filter_order)) {
$special_filters = array(
'filter-c3RhdHVz', // status
'filter-cmVzb2x1dGlvbg__', // resolution
'filter-dmlzaWJpbGl0eQ__', // visibility
'filter-cmVwb3J0ZXI_', // reporter
);
foreach ($special_filters as $f) {
if (!in_array($f, $filter_order)) {
// This filter is currently hidden
// move the tags, but with the full name
foreach($filters[$f]['options'] as $k => $v) {
$tags['regular'][$k] = $k;
}
// create an entry in the "hidden tags" list,
// this will be processed further down (and put the filter into the "hidden panel")
$tags['hidden'][$f] = array();
}
}
// resort tags
ksort($tags['regular']);
}
// build general "Tags" filter
$filters = array_merge($filters, issue_tracker_tags_filter($tags['regular'], 'tags'));
// add dynamic filters (#995)
foreach ($tags['separated'] as $prefix => $filter) {
$filters += issue_tracker_tags_filter($filter, $prefix);
}
// process hidden tags
foreach ($tags['hidden'] as $prefix => $filter) {
$filters[$prefix]['options'] = array();
$hidden_filters[] = $filters[$prefix];
unset($filters[$prefix]);
}
// Enforce filter order
// This will take care of hidden filters as well (by not adding them).
$ordered_filters = array();
if (!isset($filter_order)) {
// This variable was retrieved from Drupal, if it's null, then the filters were never configured.
$filter_order = array_keys($filters);
}
// now we build the list of visible filters
foreach ($filter_order as $filter_name) {
if (array_key_exists($filter_name, $filters)) {
$ordered_filters[] = $filters[$filter_name];
unset($filters[$filter_name]);
}
}
// Put the remaining filters into the hidden panel
// Note: this shouldn't happen (ever), as all filters should have been sorted in the code above
$hidden_filters += $filters;
// free-filters
$free_filters = array();
$free_filters[] = issue_tracker_text_filter();
global $user;
if ($user->uid > 0) {
$free_filters[] = issue_tracker_save_filter();
}
$output .= '";
return $output;
}
function issue_tracker_get_closed_status($issue_id) {
// return if this issue is 'closed' or not
// get tags
$n->nid = _nid_from_issue_id($issue_id);
$node_revs = node_revision_list($n);
$keys = array_keys($node_revs);
$latest_revision = $node_revs[$keys[0]];
$n->vid = $latest_revision->vid;
$vid = _issue_tracker_get_issue_vocabulary_id();
$tags = taxonomy_node_get_terms_by_vocabulary($n, $vid);
foreach ($tags as $t) {
if ($t->name == 'status::closed') {
$status = True;
break;
}
if ($t->name == 'status::true') {
break;
}
}
drupal_json(
array(
'issueClosedStatus' => $status,
)
);
}
/**
* Internal XML-RPC interface definition
*
* Defines mapping from xml-rpc methods to drupal functions.
* These functions are only called by the backend and NOT by drupal itself.
*/
function issue_tracker_xmlrpc() {
return array(
array(
'issue.add', // xml-rpc method
'_issue_add', // drupal function
array(
boolean, //return value (status)
int, // issue_id
string, // user name
string, // title
string, // description
string, // tags
boolean // is_private
),
t('Adds a new issue in the Drupal database')
),
array(
'issue.update',
'_issue_update',
array(
boolean, // return value (status)
int, // project issue id
string, // title
string, // description
string, // tags
boolean // is_private
),
t('Updates an existing issue in the Drupal database')
),
array(
'issue.comment',
'_issue_comment',
array(
boolean, // return value (status)
int, // project issue id
string, // user name
string, // description
string // tags
),
t('Adds an issue reply')
),
array(
'issue.add_attachment',
'_issue_add_attachment',
array(
boolean, // return value (status)
int, // project issue id
string, // filename
string, // filepath
string // description
),
t('Adds a new file attachment in the Drupal database')
),
array(
'issue.remove_attachment',
'_issue_remove_attachment',
array(
boolean, // return value (status)
int, // project issue id
string, // filename
),
t('Removes a file attachment from the Drupal database')
),
array(
'issue.get_unique_file_name',
'_issue_get_unique_file_name',
array(
string, //return value (unique file name)
string //name of a file which will be uploaded
),
t('Creates a unique file name for a file that will be uploaded')
),
array(
'issue.delete',
'_issue_delete',
array(
boolean, // return value (status)
int, // project issue id
),
t('Deletes an issue from Drupal'),
),
);
}