project_issues // issue comments -> project_issue_comments /// Default age in days of issues to auto close. define('PROJECT_ISSUE_AUTO_CLOSE_DAYS', 14); /// Project issue state = fixed. define('PROJECT_ISSUE_STATE_FIXED', 2); /// Project issue state = closed. define('PROJECT_ISSUE_STATE_CLOSED', 7); /** * Implementation of hook_init(). */ function project_issue_init() { /// @TODO: we need a real page split instead of this. module_load_include('inc', 'project_issue', 'issue'); foreach (array('comment', 'mail') as $file) { module_load_include('inc', 'project_issue', "includes/$file"); } /// @TODO: this should only be done on pages that need it. $path = drupal_get_path('module', 'project_issue'); drupal_add_css($path .'/project_issue.css'); } function project_issue_menu() { $items = array(); $includes = drupal_get_path('module', 'project_issue') .'/includes'; // Issues $items['project/issues/update_project'] = array( 'page callback' => 'project_issue_update_project', 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'type' => MENU_CALLBACK, ); $items['project/issues/statistics'] = array( 'title' => 'Statistics', 'page callback' => 'project_issue_statistics', 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/statistics.inc', ); $items['project/issues/statistics/%project_node'] = array( 'title' => 'Statistics', 'page callback' => 'project_issue_statistics', 'page arguments' => array(3), 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/statistics.inc', ); $path = 'project/issues/subscribe-mail'; if (!variable_get('project_issue_global_subscribe_page', TRUE)) { // If we don't want the global subscribe page, require an argument. $path .= '/%'; } $items[$path] = array( 'title' => 'Subscribe', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_issue_subscribe', 3), 'access callback' => 'project_issue_menu_access', 'access arguments' => array('auth'), 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/subscribe.inc', ); if (module_exists('search')) { $items['search/issues'] = array( 'title' => 'Issues', 'page callback' => 'project_issue_search_page', 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'type' => MENU_LOCAL_TASK, 'weight' => 4, ); } // "My projects" page (which shows all issues for all your projects) $items['project/user'] = array( 'title' => 'My projects', 'page callback' => 'project_issue_user_page', 'access callback' => 'project_issue_menu_access', 'access arguments' => array('auth'), 'type' => MENU_NORMAL_ITEM, 'weight' => -49, ); // Administrative pages $items['admin/project/project-issue-settings'] = array( 'title' => 'Project issue settings', 'description' => 'Specify where attachments to issues should be stored on your site, and what filename extensions should be allowed.', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_issue_settings_form'), 'access arguments' => array('administer projects'), 'weight' => 1, 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/admin.settings.inc', ); // Administer issue status settings $items['admin/project/project-issue-status'] = array( 'title' => 'Project issue status options', 'description' => 'Configure what issue status values should be used on your site.', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_issue_admin_states_form'), 'access arguments' => array('administer projects'), 'type' => MENU_NORMAL_ITEM, 'weight' => 1, 'file' => 'includes/admin.issue_status.inc' ); $items['admin/project/project-issue-status/delete'] = array( 'title' => 'Delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_issue_delete_state_confirm', 4), 'access arguments' => array('administer projects'), 'type' => MENU_CALLBACK, 'file' => 'includes/admin.issue_status.inc' ); // Issues subtab on project node edit tab. $items['node/%project_node/edit/issues'] = array( 'title' => 'Issues', 'page callback' => 'project_issue_project_edit_issues', 'page arguments' => array(1), 'access callback' => 'node_access', 'access arguments' => array('update', 1), 'type' => MENU_LOCAL_TASK, 'file' => 'includes/project_edit_issues.inc', ); $items['node/%project_node/edit/component/delete/%'] = array( 'title' => 'Delete component', 'description' => 'Delete component', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_issue_component_delete_form', 1, 5), 'access callback' => 'node_access', 'access arguments' => array('update', 1), 'type' => MENU_CALLBACK, 'file' => 'includes/project_edit_issues.inc', ); $items['node/add/project-issue/%'] = array( 'page callback' => 'node_add', 'page arguments' => array('project-issue'), 'title' => drupal_ucfirst(node_get_types('name', 'project_issue')), 'title callback' => 'check_plain', 'access callback' => 'node_access', 'access arguments' => array('create', 'project_issue'), 'page callback' => 'node_add', 'page arguments' => array(2), 'file' => 'node.pages.inc', 'file path' => drupal_get_path('module', 'node'), 'type' => MENU_CALLBACK, ); // Redirect node/add/project_issue/* to node/add/project-issue. $items['node/add/project_issue'] = array( 'page callback' => 'project_issue_add_redirect_page', 'page arguments' => array(3, 4), 'access callback' => 'node_access', 'access arguments' => array('create', 'project_issue'), 'file' => 'includes/issue_node_form.inc', 'type' => MENU_CALLBACK, ); // Autocomplete paths. // Autocomplete a comma-separated list of projects that have issues enabled. $items['project/autocomplete/issue/project'] = array( 'page callback' => 'project_issue_autocomplete_issue_project', 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'file' => 'autocomplete.inc', 'file path' => $includes, 'type' => MENU_CALLBACK, ); // Autocomplete a comma-separated list of projects from all issues a user // has either submitted or commented on. $items['project/autocomplete/issue/user/%'] = array( 'page callback' => 'project_issue_autocomplete_user_issue_project', 'page arguments' => array(4, 5), 'access callback' => 'project_issue_menu_access', 'access arguments' => array('any'), 'file' => 'autocomplete.inc', 'file path' => $includes, 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_menu_alter(). */ function project_issue_menu_alter(&$callbacks) { // Special menu item for the "first page" of submitting a new issue. // Instead of the treachery of a true multipage form, we just have // a simple form at node/add/project-issue that provides a project // selector which redirects to node/add/project-issue/[project-name]. $callbacks['node/add/project-issue']['page callback'] = 'project_issue_pick_project_page'; $callbacks['node/add/project-issue']['file'] = 'issue_node_form.inc'; $callbacks['node/add/project-issue']['file path'] = drupal_get_path('module', 'project_issue') . '/includes'; } /** * Determine access to a given type of menu item. * * @param $type * Type of menu item to check access for, can be 'any' if the current user * can access any issues, or 'auth' if the current user is authenticated and * can accses any issues. */ function project_issue_menu_access($type) { global $user; if ($type == 'auth' && empty($user->uid)) { return FALSE; } return user_access('access project issues') || user_access('access own project issues'); } function project_issue_help($path, $arg) { switch ($path) { case 'admin/help#project_issue': return '
'. t('Basic mail format:') .'
'. ''. t("Type: project\nProject: chatbox\nCategory: bug report\nVersion: cvs\nPriority: normal\nStatus: active\nComponent: code\n\nWhatever I type here will be the body of the node.\n") .''.
''. t('See the mailhandler help for more information on using the mailhandler module.') .'
'; case 'node/add#project_issue': return t('Add a new issue (bug report, feature request, etc) to an existing project.'); case 'admin/project/project-issue-status': return ''. t('Use this page to add new status options for project issues or to change or delete existing options.') .'
'. '.*?<\/code>|"\']|"[^"]*"|\'[^\']*\')*>.*?<\/a>';
$text = preg_replace_callback("/$regex/", 'project_issue_link_filter_callback', $text);
return $text;
}
}
function project_issue_link_filter_callback($matches) {
$parts = array();
if (preg_match('/^\[#(\d+)(?:-(\d+))?(@)?\]$/', $matches[0], $parts)) {
$nid = $parts[1];
$node = node_load($nid);
$include_assigned = isset($parts[3]);
if (is_object($node) && node_access('view', $node) && $node->type == 'project_issue') {
if (isset($parts[2])) {
// Pull comment id based on the comment number if we have one.
$comment_number = $parts[2];
if ($comment_id = db_result(db_query("SELECT pic.cid FROM {project_issue_comments} pic INNER JOIN {comments} c ON pic.cid = c.cid WHERE pic.nid = %d AND pic.comment_number = %d AND c.status = %d", $nid, $comment_number, COMMENT_PUBLISHED))) {
return theme('project_issue_issue_link', $node, $comment_id, $comment_number, $include_assigned);
}
}
// If we got this far there wasn't a valid comment number, so just link
// to the node instead.
return theme('project_issue_issue_link', $node, NULL, NULL, $include_assigned);
}
}
// If we haven't already returned a replacement, return the original text.
return $matches[0];
}
/**
* Implementation of hook_requirements().
* @ingroup project_issue_filter
*
* Check for conflicts with:
* installed node access control modules,
* 'access project issues' restrictions,
* filters escaping code with higher weight.
*/
function project_issue_requirements($phase) {
$requirements = array();
$input_formats = array();
if ($phase == 'runtime') {
$grants = module_implements('node_grants');
$allowed_roles = user_roles(FALSE, 'access project issues');
$conflict_anonymous = empty($allowed_roles[DRUPAL_ANONYMOUS_RID]);
foreach (filter_formats() as $format => $input_format) {
$filters = filter_list_format($format);
if (isset($filters['project_issue/0'])) {
if (!empty($grants) && filter_format_allowcache($format)) {
$requirements[] = array(
'title' => t('Project Issue to link filter'),
'value' => t('Some module conflicts were detected.'),
'description' => t('%issuefilter should not be enabled when a node access control is also in use. Users may be able to see cached titles of project issues they would otherwise not have access to. You should disable this filter in !inputformat input format.', array('%issuefilter' => t('Project Issue to link filter'), '!inputformat' => l($input_format->name, "admin/settings/filters/$format"))),
'severity' => REQUIREMENT_ERROR,
);
}
if ($conflict_anonymous && filter_format_allowcache($format)) {
$requirements[] = array(
'title' => t('Project Issue to link filter'),
'value' => t('Some security conflicts were detected.'),
'description' => t('%issuefilter conflicts with project issue access settings. Users who do not have access to all project issues may be able to see titles of project issues. You should disable this filter in !inputformat input format.', array('%issuefilter' => t('Project Issue to link filter'), '!inputformat' => l($input_format->name, "admin/settings/filters/$format"))),
'severity' => REQUIREMENT_ERROR,
);
}
// Put up an error when some code escaping filter's weight is higher.
$low_filters = array('filter/0', 'filter/1', 'bbcode/0', 'codefilter/0', 'geshifilter/0');
foreach ($low_filters as $lfilter) {
if (isset($filters[$lfilter]) && $filters['project_issue/0']->weight <= $filters[$lfilter]->weight) {
$description_names['%issuefilter'] = $filters['project_issue/0']->name;
$description_names['%lowfilter'] = $filters[$lfilter]->name;
$requirements[] = array(
'title' => t('Project Issue to link filter'),
'value' => t('Some filter conflicts were detected.'),
'description' => t('%issuefilter should come after %lowfilter to prevent loss of layout and highlighting.', $description_names) .' '. l(t('Please rearrange the filters.'), "admin/settings/filters/$format/order"),
'severity' => REQUIREMENT_ERROR,
);
}
}
}
}
}
return $requirements;
}
/**
* Implement hook_token_list() (from token.module).
*/
function project_issue_token_list($type) {
if ($type == 'node') {
$tokens['node'] = array(
'project_issue_pid' => t("The issue's project nid"),
'project_issue_project_title' => t("The issue's project title"),
'project_issue_project_title-raw' => t("The issue's project title raw"),
'project_issue_project_shortname' => t("The issue's project short name"),
'project_issue_category' => t("The issue's category (bug, feature)"),
'project_issue_component' => t("The issue's component"),
'project_issue_priority' => t("The issue's priority"),
'project_issue_version' => t("The issue's version (if any)"),
'project_issue_assigned' => t("The name of the user an issue is assigned to"),
'project_issue_status' => t("The issue's status"),
);
return $tokens;
}
}
/**
* Implement hook_token_values() (from token.module).
*/
function project_issue_token_values($type = 'all', $object = NULL) {
if ($type == 'node') {
// Defaults in case it's not an issue or we can't load its parent project.
$values = array(
'project_issue_pid' => '',
'project_issue_project_title' => '',
'project_issue_project_title-raw' => '',
'project_issue_project_shortname' => '',
'project_issue_category' => '',
'project_issue_component' => '',
'project_issue_priority' => '',
'project_issue_version' => '',
'project_issue_assigned' => '',
'project_issue_status' => '',
);
if ($object->type == 'project_issue') {
if (!empty($object->project_issue)) {
// If $node->project_issue exists, use it.
$issue = (object)$object->project_issue;
}
else {
$issue = $object;
}
if ($project = node_load($issue->pid)) {
$values['project_issue_pid'] = intval($issue->pid);
$values['project_issue_project_title'] = check_plain($project->title);
$values['project_issue_project_title-raw'] = $project->title;
$values['project_issue_project_shortname'] = check_plain($project->project['uri']);
}
if (module_exists('project_release') && !empty($issue->rid) && $release = node_load($issue->rid)) {
$values['project_issue_version'] = check_plain($release->project_release['version']);
}
if (!empty($issue->assigned)) {
$account = user_load($issue->assigned);
$values['project_issue_assigned'] = check_plain($account->name);
}
$values['project_issue_category'] = project_issue_category($issue->category, FALSE);
$values['project_issue_component'] = check_plain($issue->component);
$values['project_issue_priority'] = project_issue_priority($issue->priority);
$values['project_issue_status'] = check_plain(project_issue_state($issue->sid));
}
return $values;
}
}
/**
* Calculate the differences in project_issue comment metadata
* between the original issue and a comment or between two
* comments.
*
* @param $view
* A string representing the metadata view being generated. For the comment
* metadata table, this will be 'diff'.
* @param $node
* The issue node.
* @param $old_data
* Object containing old metadata.
* @param $new_data
* Object containing new metadata.
* @param $field_labels
* An associative array of field_name=>display_name pairs.
* In most cases, this will be the array returned by project_issue_change_summary().
*
* @return
* An associative array containing information about changes between
* the two objects.
* For example:
* array(
* 'component' => array(
* 'label' => t('Component'),
* 'old' => 'Code',
* 'new' => 'User interface',
* ),
* 'sid' => array(
* 'label' => t('Status'),
* 'old' => 8,
* 'new' => 13,
* ),
* )
*/
function project_issue_metadata_changes($node, $old_data, $new_data, $field_labels = array()) {
$changes = array();
foreach ($field_labels as $property => $name) {
if ($property == 'rid' && empty($old_data->rid) && empty($new_data->rid)) {
// Special case for version -- if both are empty, leave it out entirely,
// since maybe this project doesn't have (and/or disabled) releases.
continue;
}
if (isset($old_data->$property) || isset($new_data->$property)) {
$changes[$property] = array('label' => $name);
}
if (isset($old_data->$property) && isset($new_data->$property)) {
if ($old_data->$property != $new_data->$property) {
$changes[$property]['old'] = $old_data->$property;
$changes[$property]['new'] = $new_data->$property;
}
}
elseif (isset($old_data->$property)) {
$changes[$property]['old'] = $old_data->$property;
}
elseif (isset($new_data->$property)) {
$changes[$property]['new'] = $new_data->$property;
}
}
// Allow other modules to implement hook_project_issue_metadata() so that they
// can find changes in additional metadata. In most cases other modules will
// be responsible for storing this metadata in their own tables. Developers
// of modules that implement this hook should keep in mind the following:
// 1. Implementations of hook_project_issue_metadata() must take the
// $changes array by reference.
// 2. Differences in properties will only be processed later on for
// elements of the array which have the 'label', 'old', and 'new' properties
// defined.
// In other words, for each line in the differences table (or field in the email)
// that is displayed, your hook should add something like the following as a
// new element of the $changes array:
// 'taxonomy_vid_10' => array(
// 'label' => 'Vocabulary 10',
// 'old' => 'MySQL, pgSQL, javascript',
// 'new' => 'pgSQL, newbie',
// ),
//
// There are two methods you can use to indicate multiple changes of a field.
// The first is that for 'old' and 'new' you pass strings separated by some
// character, customarily a comma. This method is used in
// the example above. When using this method, the default display of the changes
// will be to show all old values followed by all new values. In the example
// above, this would be displayed like:
// Vocabulary 10: MySQL, pgSQL, javascript >> pgSQL, newbie
//
// The other method you can use when constructing 'old' and 'new' is to make
// both of these arrays, with each element of the array one change. If you
// use this method, all elements in the 'old' array are typically interpreted
// as being removed, and all elements in the 'new' array are typically interpreted
// as being added. An example of this type of structure is as follows:
// 'taxonomy_vid_10' => array(
// 'label' => 'Vocabulary 10',
// 'old' => array('MySQL', 'javascript'),
// 'new' => array('newbie'),
// ),
// In this situation, the default display of these changes in a project issue
// metadata table would be as follows:
// Vocabulary 10: -MySQL, -javascript +newbie
foreach (module_implements('project_issue_metadata') as $module) {
$function = $module .'_project_issue_metadata';
$function('diff', $node, $changes, $old_data, $new_data);
}
return $changes;
}
function project_issue_form_project_quick_navigate_form_alter(&$form, $form_state) {
$form['issues'] = array(
'#type' => 'submit',
'#value' => t('View issues'),
'#submit' => array('project_issue_quick_navigate_issues_submit'),
'#validate' => array('project_issue_quick_navigate_issues_validate'),
);
}
function project_issue_quick_navigate_issues_validate($form, &$form_state) {
$project = db_fetch_object(db_query("SELECT pp.uri, pip.issues FROM {project_projects} pp INNER JOIN {project_issue_projects} pip ON pp.nid = pip.nid WHERE pp.nid = %d", $form_state['values']['project_goto']));
if (empty($project)) {
form_set_error('project_goto', t('You must select a project to view issues for.'));
}
if (empty($project->issues)) {
form_set_error('project_goto', t('The selected project does not have an issue queue.'));
}
else {
$form_state['project_uri'] = $project->uri;
}
}
function project_issue_quick_navigate_issues_submit($form, &$form_state) {
$form_state['redirect'] = 'project/issues/'. $form_state['project_uri'];
}
function project_issue_form_project_quick_navigate_title_form_alter(&$form, $form_state) {
$form['issues'] = array(
'#type' => 'submit',
'#value' => t('View issues'),
'#validate' => array('project_issue_quick_navigate_title_issues_validate'),
'#submit' => array('project_issue_quick_navigate_title_issues_submit'),
);
}
function project_issue_quick_navigate_title_issues_validate($form, &$form_state) {
if (empty($form_state['values']['project_title'])) {
form_set_error('project_title', t('You must enter a project to view issues for.'));
}
else {
$project = db_fetch_object(db_query("SELECT pp.uri, pip.issues FROM {node} n INNER JOIN {project_projects} pp ON n.nid = pp.nid INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid WHERE n.title = '%s'", $form_state['values']['project_title']));
if (empty($project)) {
form_set_error('project_title', t('The name you entered (%title) is not a valid project.', array('%title' => $form_state['values']['project_title'])));
}
if (empty($project->issues)) {
form_set_error('project_title', t('The selected project does not have an issue queue.'));
}
else {
$form_state['project_uri'] = $project->uri;
}
}
}
function project_issue_quick_navigate_title_issues_submit($form, &$form_state) {
$form_state['redirect'] = 'project/issues/'. $form_state['project_uri'];
}
function project_issue_alter_views_exposed_form(&$form, &$form_state) {
switch ($form_state['view']->name) {
case 'project_issue_search_all':
case 'project_issue_search_project':
case 'project_issue_user_projects':
$user_filters = array('assigned', 'submitted', 'participant');
foreach (element_children($form) as $element) {
if ($form[$element]['#type'] == 'textfield') {
if ($form_state['view']->name == 'project_issue_user_projects') {
$form[$element]['#size'] = 16;
}
else {
$form[$element]['#size'] = 32;
}
}
elseif ($form[$element]['#type'] == 'select' && $form[$element]['#multiple']) {
$form[$element]['#size'] = 5;
}
if (in_array($element, $user_filters)) {
$form[$element]['#description'] = t('Enter a comma separated list of users.');
}
}
break;
}
// Rename the "Apply" button to "Search" on all project_issue_* views.
if (substr($form_state['view']->name, 0, 14) == 'project_issue_') {
$form['submit']['#value'] = t('Search');
}
}
function project_issue_preprocess_views_view_table($variables) {
$view = $variables['view'];
if ($view->plugin_name == 'project_issue_table') {
foreach ($view->result as $num => $result) {
$variables['row_classes'][$num][] = "state-$result->project_issues_sid";
$variables['row_classes'][$num][] = "priority-$result->project_issues_priority";
}
$variables['class'] .= " project-issue";
}
}
/**
* Generate the links used at the top of query result pages.
*
* @param $project_arg
* The node ID or project short name (uri) of the project to generate links
* for, or NULL if it's a page of site-wide issues.
*
* @return
* Themed HTML output for the list of links.
*
* @see theme_project_issue_query_result_links()
*/
function project_issue_query_result_links($project_arg = NULL) {
global $user;
$links = array();
if (empty($project_arg)) {
// These are site-wide links, not per-project
if (node_access('create', 'project_issue')) {
$links['create'] = array(
'title' => t('Create a new issue'),
'href' => "node/add/project-issue",
'attributes' => array('title' => t('Create a new issue.')),
);
}
else {
$links['create'] = array(
'title' => theme('project_issue_create_forbidden'),
'html' => TRUE,
);
}
$links['search'] = array(
'title' => t('Advanced search'),
'href' => "project/issues/search",
'attributes' => array('title' => t('Use the advanced search page for finding issues.')),
);
$links['statistics'] = array(
'title' => t('Statistics'),
'href' => "project/issues/statistics",
'attributes' => array('title' => t('See statistics about issues.')),
);
if (!empty($user->uid) && variable_get('project_issue_global_subscribe_page', TRUE)) {
$links['subscribe'] = array(
'title' => t('Subscribe'),
'href' => "project/issues/subscribe-mail",
'attributes' => array('title' => t('Receive e-mail updates about issues.')),
);
}
}
else {
// We know the project, make project-specific links.
if (is_numeric($project_arg)) {
$uri = project_get_uri_from_nid($project_arg);
}
else {
$uri = $project_arg;
}
if (node_access('create', 'project_issue')) {
$links['create'] = array(
'title' => t('Create a new issue'),
'href' => "node/add/project-issue/$uri",
'attributes' => array('title' => t('Create a new issue for @project.', array('@project' => $uri))),
);
}
else {
$links['create'] = array(
'title' => theme('project_issue_create_forbidden', $uri),
'html' => TRUE,
);
}
$links['search'] = array(
'title' => t('Advanced search'),
'href' => "project/issues/search/$uri",
'attributes' => array('title' => t('Use the advanced search page to find @project issues.', array('@project' => $uri))),
);
$links['statistics'] = array(
'title' => t('Statistics'),
'href' => "project/issues/statistics/$uri",
'attributes' => array('title' => t('See statistics about @project issues.', array('@project' => $uri))),
);
if ($user->uid) {
$links['subscribe'] = array(
'title' => t('Subscribe'),
'href' => "project/issues/subscribe-mail/$uri",
'attributes' => array('title' => t('Receive e-mail updates about @project issues.', array('@project' => $uri))),
);
}
}
return theme('project_issue_query_result_links', $links);
}
/**
* Helper function to return an array of projects that meet a given constraint.
*
* @param $constraint
* Restrict the list of projects. Valid options are 'all' (all projects
* with issue tracking enabled), 'owner' (all projects owned by a given
* user) and 'participant' (all projects from issues that a given user
* submitted or commented on.
* @param $uid
* User ID to use for $constraint == 'owner' or 'participant'.
*
* @return
* Array of project titles, keyed by node ID (nid) that match the given
* constraint.
*/
function project_issue_get_projects($constraint = 'all', $uid = NULL) {
$options = array();
$join = '';
// Only published projects.
$where[] = 'n.status = %d';
$args[] = 1;
// That have issue tracking enabled.
$where[] = 'pip.issues = %d';
$args[] = 1;
// Add extra JOIN and WHERE depending on the requested project source.
switch ($constraint) {
case 'owner':
// The given uid must own each project.
$where[] = 'n.uid = %d';
$args[] = $uid;
break;
case 'participant':
$join = 'INNER JOIN {project_issues} pi ON n.nid = pi.pid INNER JOIN {node} pin ON pi.nid = pin.nid LEFT JOIN {comments} c ON pi.nid = c.nid';
// Restrict to published issues...
$where[] = 'pin.status = %d';
$args[] = 1;
// ...that this user submitted or commented on.
$where[] = '(pin.uid = %d OR c.uid = %d)';
$args[] = $uid;
$args[] = $uid;
break;
}
// Build the actual query.
$sql = "SELECT n.nid, n.title FROM {node} n INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid $join WHERE " . implode(' AND ', $where) . " ORDER BY n.title ASC";
$query = db_query(db_rewrite_sql($sql), $args);
while ($project = db_fetch_object($query)) {
$options[$project->nid] = $project->title;
}
return $options;
}
/**
* Menu access callback for the project_issue_plugin_access_user_list plugin.
*/
function project_issue_views_user_access($view_name, $display_id, $argument_name) {
$view = views_get_view($view_name);
$view->set_display($display_id);
$view->init_handlers();
// Find the values for any arguments embedded in the path via '%'.
$i = 0;
foreach (explode('/', $view->display_handler->get_option('path')) as $element) {
if ($element == '%') {
$view->args[] = arg($i);
}
$i++;
}
// Now handle any implicit arguments from the end of the path.
$num_arguments = count($view->argument);
while (count($view->args) < $num_arguments) {
$view->args[] = arg($i);
$i++;
}
$arg_uid = $view->argument[$argument_name]->get_value();
return !empty($arg_uid);
}
/**
* Return the views filter identifier for a given project issue vocabulary.
*/
function project_issue_views_filter_identifier($name) {
return drupal_strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', check_plain($name)));
}
/**
* Implementation of hook_block().
*/
function project_issue_block($op = 'list', $delta = 0, $edit = array()) {
if ($op == 'list') {
$blocks['issue_cockpit'] = array(
'info' => t('Issue cockpit'),
'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE,
);
return $blocks;
}
elseif ($op == 'configure' && $delta == 'issue_cockpit') {
$options = array('All' => t('All issues')) + project_issue_category();
$form['project_issue_cockpit_categories'] = array(
'#type' => 'checkboxes',
'#title' => t('Issue categories to display'),
'#description' => t('Select which categories the block should display a summary of total vs. open issues.'),
'#default_value' => variable_get('project_issue_cockpit_categories', array('All' => 'All', 'bug' => 'bug')),
'#options' => $options,
);
return $form;
}
elseif ($op == 'save' && $delta == 'issue_cockpit') {
variable_set('project_issue_cockpit_categories', $edit['project_issue_cockpit_categories']);
// Invalidate the cache for this block since the categories might change.
cache_clear_all('project_issue_cockpit_block:', 'cache', TRUE);
}
elseif ($op == 'view' && ($node = project_get_project_from_menu()) && !empty($node->project_issue['issues']) && node_access('view', $node)) {
$cid = 'project_issue_cockpit_block:'. $node->nid;
if (($cache = cache_get($cid))) {
$block = $cache->data;
}
else {
module_load_include('inc', 'project_issue', 'includes/issue_cockpit');
$block = array(
'subject' => t('Issues for @project', array('@project' => $node->title)),
'content' => theme('project_issue_issue_cockpit', $node),
);
cache_set($cid, $block);
}
return $block;
}
}
/**
* Implemenation of hook_project_page_link_alter().
*
* Add project_issue-specific links to the array of project links.
*/
function project_issue_project_page_link_alter(&$links, $node) {
// TODO: Assumes "patch" issue status values (http://drupal.org/node/27865).
// Prepend a link to the development section to view pending patches.
if (!empty($node->project['uri'])) {
$patches['pending_patches'] = l(t('View pending patches'), 'project/issues/search/'. $node->project['uri'], array('query' => 'status[]=8&status[]=13&status[]=14'));
$links['development']['links'] = $patches + $links['development']['links'];
}
}