diff --git a/includes/admin.settings.inc b/includes/admin.settings.inc index c65c20c..90a6d73 100644 --- a/includes/admin.settings.inc +++ b/includes/admin.settings.inc @@ -26,6 +26,21 @@ function project_issue_settings_form(&$form_state) { '#description' => t('If selected, user signatures will be appended to the display of issue followups.'), ); + if (module_exists('flag')) { + $flags = flag_get_flags(); + $flag_options[0] = t('- None -'); + foreach ($flags as $flag) { + $flag_options[$flag->name] = $flag->title; + } + $form['project_issue_subscription_flag'] = array( + '#title' => t('Issue subscription flag'), + '#type' => 'select', + '#options' => $flag_options, + '#default_value' => variable_get('project_issue_subscription_flag', NULL), + '#description' => t('Allows users to receive e-mail notifications about issues that have been flagged with the selected flag. When none is selected, users can only subscribe by commenting on an issue.'), + ); + } + $form['project_issue_reply_to'] = array( '#type' => 'textfield', '#title' => t('Reply-to address on e-mail notifications'), diff --git a/includes/autocomplete.inc b/includes/autocomplete.inc index 96afb5b..fd15366 100644 --- a/includes/autocomplete.inc +++ b/includes/autocomplete.inc @@ -71,76 +71,3 @@ function project_issue_autocomplete_user_issue_project($uid, $string = '') { drupal_json($matches); } -/** - * Handles the auto-complete callback for the nodereference widget. - * - * Instead of returning a value, this function sends it to the browser by - * calling drupal_json(). The returned value is a JSON-encoded array of matches, - * where the array keys and values are both '#NID: TITLE', where NID is the node - * ID, and title is the node title. In the values, the title is run through - * check_plain, but not in the keys. - * - * @param $string - * String the user typed. - */ -function project_issue_autocomplete_issues_nodereference($string) { - $matches = array(); - $results = project_issue_autocomplete_issues_search($string); - foreach ($results as $nid => $title) { - // NID here is coming from the node table, so doesn't need to be - // sanitized. In the array key, we don't sanitize the title either, - // because that is what the nodereference module is expecting, but we - // do want it sanitized in the output. - $matches["#$nid: " . $title] = "#$nid: " . check_plain($title); - } - drupal_json($matches); -} - -/** - * Matches issues against input (partial node ID or partial title). - * - * @param $string - * User submitted text to match against the issue node ID or title. - * @param $items - * Number of matches to return. - * - * @return - * Associative array of issues that match the input string, with node ID as - * the key, and title as the value. If there are no matches, an empty array - * is returned. - */ -function project_issue_autocomplete_issues_search($string, $items = 10) { - $matches = array(); - - // Match against node IDs first. - if (is_numeric($string)) { - // Try to find issues whose ID starts with this number. - $result = db_query_range(db_rewrite_sql("SELECT DISTINCT(n.nid), n.title FROM {node} n WHERE n.status = 1 AND n.type = 'project_issue' AND n.nid LIKE '%s%'"), $string, 0, $items); - } - while ($issue = db_fetch_object($result)) { - $matches[$issue->nid] = $issue->title; - } - - // If we don't have the required number of items, match against the title, - // using a full-text match of whatever was entered. - if (count($matches) < $items) { - $needed = $items - count($matches); - $values = array(); - // Make sure that any matches that we've already found are excluded. - if (!empty($matches)) { - $values = array_keys($matches); - $sql = "SELECT n.nid, n.title FROM {node} n WHERE n.status = 1 AND n.type = 'project_issue' AND n.nid NOT IN (" . db_placeholders($values) . ") AND n.title LIKE '%%%s%%'"; - } - else { - $sql = "SELECT n.nid, n.title FROM {node} n WHERE n.status = 1 AND n.type = 'project_issue' AND n.title LIKE '%%%s%%'"; - } - // We need the string to match against. - $values[] = $string; - $result = db_query_range(db_rewrite_sql($sql), $values, 0, $needed); - } - while ($issue = db_fetch_object($result)) { - $matches[$issue->nid] = $issue->title; - } - - return $matches; -} diff --git a/includes/mail.inc b/includes/mail.inc index 0e510fc..7efe394 100644 --- a/includes/mail.inc +++ b/includes/mail.inc @@ -249,15 +249,101 @@ function project_issue_mail_notify($nid) { } } - if (count($uids)) { + // Retrieve list of users being globally subscribed to all issues. + $accounts_all_issues = array(); + $result = db_query("SELECT pisu.uid, u.name, u.mail FROM {project_issue_subscriptions_user} pisu INNER JOIN {users} u ON pisu.uid = u.uid WHERE u.status = 1 AND pisu.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_ALL); + while ($account = db_fetch_object($result)) { + $accounts_all_issues[$account->uid] = $account; + } + + // Check whether Flag module integration is enabled for e-mail notifications. + $flag_integration = (module_exists('flag') && variable_get('project_issue_subscription_flag', 0)); + + // Retrieve list of users being globally subscribed to "own" issues (without + // Flag integration). + $accounts_flagged_issues = array(); + if (!$flag_integration) { + $result = db_query("SELECT pisu.uid, u.name, u.mail FROM {project_issue_subscriptions_user} pisu INNER JOIN {users} u ON pisu.uid = u.uid WHERE u.status = 1 AND pisu.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED); + while ($account = db_fetch_object($result)) { + $accounts_flagged_issues[$account->uid] = $account; + } + } + + // Check per-project subscriptions of involved users. + if (!empty($uids)) { $placeholders = implode(',', array_fill(0, count($uids), '%d')); - array_unshift($uids, $node->project_issue['pid']); - $result = db_query("SELECT p.*, u.uid, u.name, u.mail FROM {project_subscriptions} p INNER JOIN {users} u ON p.uid = u.uid WHERE u.status = 1 AND p.nid = %d AND (p.level = 2 OR (p.level = 1 AND u.uid IN ($placeholders)))", $uids); + $args = $uids; + array_unshift($args, $node->project_issue['pid']); + + // Check which involved users are subscribed to all issues of the project. + $result = db_query("SELECT pisp.uid, u.name, u.mail + FROM {project_issue_subscriptions_project} pisp + INNER JOIN {users} u ON pisp.uid = u.uid + WHERE u.status = 1 AND pisp.nid = %d AND pisp.uid IN ($placeholders) AND pisp.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_ALL, $args); + while ($account = db_fetch_object($result)) { + $accounts_all_issues[$account->uid] = $account; + } + + // Check which involved users are subscribed to "own" issues of the project + // (without Flag integration). + if (!$flag_integration) { + $result = db_query("SELECT pisp.uid, u.name, u.mail + FROM {project_issue_subscriptions_project} pisp + INNER JOIN {users} u ON pisp.uid = u.uid + WHERE u.status = 1 AND pisp.nid = %d AND pisp.uid IN ($placeholders) AND pisp.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED, $args); + while ($account = db_fetch_object($result)) { + $accounts_flagged_issues[$account->uid] = $account; + } + } } - else { - $result = db_query('SELECT p.*, u.uid, u.name, u.mail FROM {project_subscriptions} p INNER JOIN {users} u ON p.uid = u.uid WHERE u.status = 1 AND p.nid = %d AND p.level = 2', $node->project_issue['pid']); + + // Check which users subscribed to the issue via Flag. + if ($flag_integration) { + // Retrieve all users who flagged the issue. + $flag_contents = flag_get_content_flags('node', $node->nid, variable_get('project_issue_subscription_flag', 0)); + + // Now use the list of users that flagged the issue to retrieve the list of + // users who are subscribed to flagged issues of the project. For example, a + // user who globally subscribed to no issues should not get an e-mail + // notification after flagging the issue. That should only happen when the + // user subscribed to flagged issues for the project. + if (!empty($flag_contents)) { + $flags_uids = array_keys($flag_contents); + $placeholders = implode(',', array_fill(0, count($flags_uids), '%d')); + $args = $flags_uids; + + // Check which users flagged the issue and globally subscribed to flagged + // issues (across projects). + $result = db_query("SELECT pisu.uid, u.name, u.mail + FROM {project_issue_subscriptions_user} pisu + INNER JOIN {users} u ON pisu.uid = u.uid + WHERE u.status = 1 AND pisu.uid IN ($placeholders) AND pisu.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED, $args); + while ($account = db_fetch_object($result)) { + $accounts_flagged_issues[$account->uid] = $account; + } + + // Check which users flagged the issue and subscribed to flagged issues of + // the project. + array_unshift($args, $node->project_issue['pid']); + $result = db_query("SELECT pisp.uid, u.name, u.mail + FROM {project_issue_subscriptions_project} pisp + INNER JOIN {users} u ON pisp.uid = u.uid + WHERE u.status = 1 AND pisp.nid = %d AND pisp.uid IN ($placeholders) AND pisp.level = " . PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED, $args); + while ($account = db_fetch_object($result)) { + $accounts_flagged_issues[$account->uid] = $account; + } + } } + // Lastly, join the lists of users being + // - globally subscribed to all issues + // - globally subscribed to own issues (without Flag integration) + // - globally subscribed to flagged issues + // - subscribed to all issues of the project + // - subscribed to own issues of the project (without Flag integration) + // - subscribed to flagged issues of the project + $recipients = $accounts_all_issues + $accounts_flagged_issues; + // To save workload, check here if either the anonymous role or the // authenticated role has the 'view uploaded files' permission, since // we only need to process each user's file access permission if this @@ -289,7 +375,7 @@ function project_issue_mail_notify($nid) { // some punctuation) are used. See example in Appendix A.1.2. $from = '"' . mime_header_encode($sender->name) . "\" <$sender->mail>"; - while ($recipient = db_fetch_object($result)) { + foreach ($recipients as $recipient) { // To save work, only go through a user_load if we need it. if ($check_file_perms || $check_node_access) { $account = user_load(array('uid' => $recipient->uid)); diff --git a/includes/project_node.inc b/includes/project_node.inc index ce6c661..f62e18a 100644 --- a/includes/project_node.inc +++ b/includes/project_node.inc @@ -65,6 +65,6 @@ function project_issue_project_delete($node) { node_delete($issue->nid); } db_query('DELETE FROM {project_issue_projects} WHERE nid = %d', $node->nid); - db_query('DELETE FROM {project_subscriptions} WHERE nid = %d', $node->nid); + db_query('DELETE FROM {project_issue_subscriptions_project} WHERE nid = %d', $node->nid); } diff --git a/includes/subscribe.inc b/includes/subscribe.inc index 703114b..d58703c 100644 --- a/includes/subscribe.inc +++ b/includes/subscribe.inc @@ -1,172 +1,179 @@ t('None'), + PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED => t('Subscribed issues'), + PROJECT_ISSUE_SUBSCRIPTIONS_ALL => t('All issues'), + ); +} - if (!valid_email_address($user->mail)) { - drupal_set_message(t('You need to provide a valid e-mail address to subscribe to issue e-mails. Please edit your user information.'), 'error'); - drupal_goto('user/'. $user->uid .'/edit'); - } +/** + * Form constructor for global user project issue subscriptions. + */ +function project_issue_subscriptions_user_form(&$form_state, $account) { + drupal_set_title(t('My subscriptions')); - $levels = array(0 => t('None'), 1 => t('Own issues'), 2 => t('All issues')); + // Global subscription level. + project_issue_subscriptions_user_settings_load($account); - if ($project_nid) { - if (!is_numeric($project_nid)) { - $project_nid = db_result(db_query(db_rewrite_sql("SELECT p.nid FROM {project_projects} p WHERE p.uri = '%s'", 'p'), $project_nid)); - } - if (!$project_nid) { - return drupal_not_found(); - } + $form['account'] = array( + '#type' => 'value', + '#value' => $account, + ); + $form['#tree'] = TRUE; - $project = node_load($project_nid); - project_project_set_breadcrumb($project, TRUE); + $form['project_issue_subscriptions'] = array( + '#type' => 'fieldset', + '#title' => t('Issue subscriptions'), + '#collapsible' => TRUE, + ); - $level = db_result(db_query('SELECT level FROM {project_subscriptions} WHERE nid = %d AND uid = %d', $project->nid, $user->uid)); - $form['single'] = array( - '#type' => 'value', - '#value' => $project->nid, - ); - $form['#project'] = array( - '#type' => 'value', - '#value' => $project, - ); - $form['subscribe'] = array( - '#type' => 'markup', - '#value' => '

'. t('Subscribe to receive e-mail notification when an issue for this project is updated.') .'

', + $form['project_issue_subscriptions']['level'] = array( + '#type' => 'radios', + '#title' => t('Global project issue subscription level'), + '#options' => _project_issue_subscription_levels(), + '#default_value' => $account->project_issue_subscriptions['level'], + ); + + // Per-project subscription level (only enabled). + // We only allow to change (and remove) per-project subscriptions on this + // form. Users are able to subscribe to further projects by visiting the + // individual project pages. In terms of UX, that's preferred anyway, since + // a user normally wants to know and be sure what exactly she subscribes to. + $form['project_issue_subscriptions']['projects'] = array( + '#theme' => 'project_issue_subscriptions_projects_table', + '#header' => array(t('Project'), t('Subscription level')), + ); + $result = db_query(db_rewrite_sql("SELECT pisp.nid, n.title, pisp.level + FROM {project_issue_subscriptions_project} pisp + INNER JOIN {node} n ON n.nid = pisp.nid + WHERE n.type = 'project_project' AND n.status = 1 AND pisp.uid = %d ORDER BY n.title + ", 'n'), $account->uid); + while ($project = db_fetch_object($result)) { + $form['project_issue_subscriptions']['projects'][$project->nid]['title'] = array( + '#value' => l($project->title, "node/$project->nid"), ); - $form['options']['#tree'] = TRUE; - $form['options'][$project->nid] = array( - '#type' => 'radios', - '#title' => t('Subscribe to @project issues', array('@project' => $project->title)), - '#default_value' => isset($level) ? $level : 0, - '#options' => $levels, + $form['project_issue_subscriptions']['projects'][$project->nid]['level'] = array( + '#type' => 'select', + '#options' => $form['project_issue_subscriptions']['level']['#options'], + '#default_value' => $project->level, + '#parents' => array('project_issue_subscriptions', 'projects', $project->nid), ); - } - else { - $form['buttons']['all'] = array( - '#type' => 'markup', - '#value' => t('All projects'), - ); - foreach ($levels as $key => $level) { - $form['buttons'][$level] = array( - '#type' => 'submit', - '#name' => 'all', - '#value' => $level, - ); - } - $nids = array(); - - $result = db_query(db_rewrite_sql("SELECT s.nid, n.title, s.level, p.uri FROM {project_subscriptions} s INNER JOIN {node} n ON n.nid = s.nid INNER JOIN {project_projects} p ON n.nid = p.nid WHERE n.type = 'project_project' AND n.status = 1 AND s.uid = %d ORDER BY n.title", 's'), $user->uid); - while ($project = db_fetch_object($result)) { - $form['project'][$project->nid]['title'] = array( - '#value' => l($project->title, "node/$project->nid"), - ); - foreach ($levels as $key => $level) { - if ($project->level == $key) { - $status[$project->nid] = $key; - } - } - $nids[] = $project->nid; - } - - if (empty($nids)) { - $placeholders = ''; - } - else { - $placeholders = " AND n.nid NOT IN (". implode(',', array_fill(0, count($nids), '%d')) .")"; - } - - $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, p.uri FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid WHERE n.type = 'project_project' AND n.status = 1". ($nids ? $placeholders : "") ." ORDER BY n.title"), $nids); - while ($project = db_fetch_object($result)) { - $form['project'][$project->nid]['title'] = array( - '#value' => l($project->title, "node/$project->nid"), - ); - $nids[] = $project->nid; - } - - foreach ($nids as $nid) { - $form['options']['#tree'] = TRUE; - $form['options'][$nid] = array( - '#type' => 'radios', - '#default_value' => isset($status[$nid]) ? $status[$nid] : 0, - '#options' => $levels, - ); - } - } - $form['submit'] = array( + $form['actions']['submit'] = array( '#type' => 'submit', - '#value' => t('Subscribe'), + '#value' => t('Save'), + '#weight' => 100, ); return $form; } -function theme_project_issue_subscribe($form) { - global $user; - - $output = ''; - - if (empty($form['#project'])) { - $output .= project_issue_query_result_links(); - } - else { - $project = $form['#project']['#value']; - $output .= project_issue_query_result_links($project->project['uri']); +/** + * Form submission handler for project_issue_user_subscribe_form(). + */ +function project_issue_subscriptions_user_form_submit($form, &$form_state) { + // Update the global issue subscription settings. + $account = $form_state['values']['account']; + $account->project_issue_subscriptions = $form_state['values']['project_issue_subscriptions']; + project_issue_subscriptions_user_settings_save($account); + + // Insert the new per-project settings. + if (!empty($form_state['values']['project_issue_subscriptions']['projects'])) { + foreach ($form_state['values']['project_issue_subscriptions']['projects'] as $nid => $level) { + if ($level > PROJECT_ISSUE_SUBSCRIPTIONS_NONE) { + project_issue_subscriptions_project_setting_save($account->uid, $nid, $level); + } + else { + project_issue_subscriptions_project_setting_delete($account->uid, $nid); + } + } } - if (!isset($form['single'])) { - $levels = array(0 => t('None'), 1 => t('Own issues'), 2 => t('All issues')); - $headers = array_merge(array(t('Project')), $levels); + drupal_set_message(t('Your subscription settings have been updated.')); +} +/** + * Returns HTML for per-project subscription levels table in project_issue_user_subscribe_form(). + */ +function theme_project_issue_subscriptions_projects_table($element) { + $output = ''; + $rows = array(); + foreach (element_children($element) as $nid) { $row = array(); - foreach (element_children($form['buttons']) as $key) { - $row[] = drupal_render($form['buttons'][$key]); - } - $rows = array($row); - - foreach (element_children($form['project']) as $key) { - $row = array(drupal_render($form['project'][$key]['title'])); - foreach ($levels as $level => $name) { - $row[] = drupal_render($form['options'][$key][$level]); - } - $rows[] = $row; - } - $output .= theme('table', $headers, $rows); + $row[] = drupal_render($element[$nid]['title']); + $row[] = drupal_render($element[$nid]['level']); + $rows[] = $row; + } + if (!empty($rows)) { + $output .= theme('table', $element['#header'], $rows); } - $output .= drupal_render($form); + $output .= drupal_render($element); return $output; } -function project_issue_subscribe_submit($form, &$form_state) { - +/** + * Form constructor for per-project issue subscription. + */ +function project_issue_subscriptions_project_form($form_state, $project_nid) { global $user; - $all = $form_state['clicked_button']['#value']; - $levels = array(0 => t('None'), 1 => t('Own issues'), 2 => t('All issues')); + // @todo Isn't this globally enforced elsewhere...? + if (!valid_email_address($user->mail)) { + drupal_set_message(t('You need to provide a valid e-mail address to subscribe to issue e-mails. Please edit your user information.'), 'error'); + drupal_goto('user/'. $user->uid .'/edit'); + } - // Remove previous subscriptions for user. - if (isset($form_state['values']['single'])) { - db_query('DELETE FROM {project_subscriptions} WHERE nid = %d AND uid = %d', $form_state['values']['single'], $user->uid); - } - else { - db_query('DELETE FROM {project_subscriptions} WHERE uid = %d', $user->uid); - } + if (!is_numeric($project_nid)) { + $project_nid = project_get_nid_from_uri($project_nid); + } + if (!$project_nid || !($project = node_load($project_nid))) { + return drupal_not_found(); + } - $_level = array_search($all, $levels); + project_project_set_breadcrumb($project, TRUE); + drupal_set_title(t('Subscribe to @project issues', array('@project' => $project->title))); - foreach ($form_state['values']['options'] as $nid => $level) { - if ($_level !== 0 && $level !== 0) { - db_query('INSERT INTO {project_subscriptions} (nid, uid, level) VALUES (%d, %d, %d)', $nid, $user->uid, $_level ? $_level : $level); - } - } - drupal_set_message(t('Subscription settings saved.')); + $form['nid'] = array( + '#type' => 'value', + '#value' => $project->nid, + ); + $form['uid'] = array( + '#type' => 'value', + '#value' => $user->uid, + ); - if (isset($form['single'])) { - $form_state['redirect'] = 'project/issues/subscribe-mail/'. $form['#project']['#value']->project['uri']; - } - else { - $form_state['redirect'] = 'project/issues/subscribe-mail'; - } + $level = project_issue_subscriptions_project_setting_load($user->uid, $project->nid); + $form['level'] = array( + '#type' => 'radios', + '#title' => t('Subscription level'), + '#options' => _project_issue_subscription_levels(), + '#default_value' => $level, + '#description' => t('Receive an e-mail notification when an issue for this project is updated.'), + ); + + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Subscribe'), + ); + return $form; +} + +/** + * Form submission handler for project_issue_user_subscribe_form(). + */ +function project_issue_subscriptions_project_form_submit($form, &$form_state) { + if ($form_state['values']['level'] > PROJECT_ISSUE_SUBSCRIPTIONS_NONE) { + project_issue_subscriptions_project_setting_save($form_state['values']['uid'], $form_state['values']['nid'], $form_state['values']['level']); + } + else { + project_issue_subscriptions_project_setting_delete($form_state['values']['uid'], $form_state['values']['nid']); + } + + drupal_set_message(t('Your subscription setting has been updated.')); } diff --git a/project_issue.info b/project_issue.info index 2225d1a..a70606e 100644 --- a/project_issue.info +++ b/project_issue.info @@ -1,9 +1,10 @@ name = Project issue tracking description = Provides issue tracking for the project.module. +package = Project +core = 6.x dependencies[] = project dependencies[] = views dependencies[] = comment dependencies[] = comment_upload dependencies[] = upload -package = Project -core = 6.x +recommends[] = flag diff --git a/project_issue.install b/project_issue.install index 1c24aff..80ef88d 100644 --- a/project_issue.install +++ b/project_issue.install @@ -282,7 +282,32 @@ function project_issue_schema() { 'primary key' => array('priority'), ); - $schema['project_subscriptions'] = array( + $schema['project_issue_subscriptions_user'] = array( + 'description' => 'Stores global issue subscriptions per user.', + 'fields' => array( + 'uid' => array( + 'description' => 'The {users}.uid for this subscriber.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'level' => array( + 'description' => 'The global subscription setting level. 0 = None, 1 = Flagged, 2 = All.', + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('uid'), + 'indexes' => array( + 'uid_level' => array('uid', 'level'), + ), + ); + + $schema['project_issue_subscriptions_project'] = array( 'description' => 'Table keeping track of per-user project_issue subscriptions.', 'fields' => array( 'nid' => array( @@ -308,6 +333,7 @@ function project_issue_schema() { 'default' => 0, ), ), + 'primary key' => array('uid', 'nid'), 'indexes' => array( 'project_subscriptions_nid_uid_level' => array('nid', 'uid', 'level'), ), @@ -626,3 +652,50 @@ function project_issue_update_6006() { return $ret; } + +/** + * Revamp project issue subscriptions for Flag integration. + */ +function project_issue_update_6007() { + $ret = array(); + + // Delete obsolete 'project_issue_global_subscribe_page' variable. + variable_del('project_issue_global_subscribe_page'); + + // Rename {project_subscriptions} to {project_issue_subscriptions_project}. + db_rename_table($ret, 'project_subscriptions', 'project_issue_subscriptions_project'); + + // Ensure that existing project subscriptions are clean. + $ret[] = update_sql("DELETE FROM {project_issue_subscriptions_project} WHERE level = 0"); + + // Add proper primary key, enforcing uniqueness. + db_add_primary_key($ret, 'project_issue_subscriptions_project', array('uid', 'nid')); + + // Create new global issue subscription setting table. + db_create_table($ret, 'project_issue_subscriptions_user', array( + 'description' => 'Stores global issue subscriptions per user.', + 'fields' => array( + 'uid' => array( + 'description' => 'The {users}.uid for this subscriber.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'level' => array( + 'description' => 'The global subscription setting level. 0 = None, 1 = Flagged, 2 = All.', + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('uid'), + 'indexes' => array( + 'uid_level' => array('uid', 'level'), + ), + )); + + return $ret; +} diff --git a/project_issue.module b/project_issue.module index 7c57e27..9d9aded 100644 --- a/project_issue.module +++ b/project_issue.module @@ -11,6 +11,21 @@ define('PROJECT_ISSUE_STATE_FIXED', 2); define('PROJECT_ISSUE_STATE_CLOSED', 7); /** + * Project issue subscriptions level: Not subscribed. + */ +define('PROJECT_ISSUE_SUBSCRIPTIONS_NONE', 0); + +/** + * Project issue subscriptions level: Own/flagged issues. + */ +define('PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED', 1); + +/** + * Project issue subscriptions level: All issues. + */ +define('PROJECT_ISSUE_SUBSCRIPTIONS_ALL', 2); + +/** * Implementation of hook_init(). */ function project_issue_init() { @@ -61,20 +76,28 @@ function project_issue_menu() { '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( + + // Project issue subscriptions. + $items['project/issues/subscribe-mail/%'] = array( 'title' => 'Subscribe', 'page callback' => 'drupal_get_form', - 'page arguments' => array('project_issue_subscribe', 3), + 'page arguments' => array('project_issue_subscriptions_project_form', 3), 'access callback' => 'project_issue_menu_access', 'access arguments' => array('auth'), 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/subscribe.inc', ); + $items['user/%user/subscriptions'] = array( + 'title' => 'Subscriptions', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_issue_subscriptions_user_form', 1), + 'access callback' => 'project_issue_menu_access', + 'access arguments' => array('auth'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'includes/subscribe.inc', + ); + + // Search. if (module_exists('search')) { $items['search/issues'] = array( 'title' => 'Issues', @@ -217,16 +240,6 @@ function project_issue_menu() { 'type' => MENU_CALLBACK, ); - // Autocomplete an issue nid from a user entered node ID or issue title. - $items['project/autocomplete/issues/nodereference'] = array( - 'page callback' => 'project_issue_autocomplete_issues_nodereference', - 'access callback' => 'project_issue_menu_access', - 'access arguments' => array('any'), - 'file' => 'autocomplete.inc', - 'file path' => $includes, - 'type' => MENU_CALLBACK, - ); - return $items; } @@ -307,10 +320,10 @@ function project_issue_theme() { 'change' => NULL, ), ), - 'project_issue_subscribe' => array( - 'file' => 'issue.inc', + 'project_issue_subscriptions_projects_table' => array( + 'file' => 'includes/subscribe.inc', 'arguments' => array( - 'form' => NULL, + 'element' => NULL, ), ), 'project_issue_summary' => array( @@ -389,14 +402,6 @@ function project_issue_theme() { 'file' => 'includes/issue_cockpit.inc', 'template' => 'theme/project-issue-issue-cockpit', ), - 'project_issue_formatter_issue_id' => array( - 'arguments' => array('element' => NULL), - 'function' => 'theme_project_issue_formatter_issue_id', - ), - 'project_issue_formatter_issue_id_assigned' => array( - 'arguments' => array('element' => NULL), - 'function' => 'theme_project_issue_formatter_issue_id_assigned', - ), ); } @@ -884,6 +889,144 @@ function _project_issue_followup_get_user() { } /** + * Loads user account settings for project issue subscriptions. + * + * @param $account + * A user account object to attach project issue subscription settings to. + * Required properties: + * - uid: The ID of the user account. + * Attached properties: + * - project_issue_subscriptions: An associative array containing: + * - level: An integer denoting the user's global issue subscription level: + * - PROJECT_ISSUE_SUBSCRIPTIONS_NONE + * - PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED + * - PROJECT_ISSUE_SUBSCRIPTIONS_ALL + */ +function project_issue_subscriptions_user_settings_load($account) { + // Setup defaults. + $defaults = array( + 'level' => PROJECT_ISSUE_SUBSCRIPTIONS_NONE, + ); + + // For existing accounts, load account settings. + if (!empty($account->uid)) { + $settings = db_fetch_array(db_query("SELECT * FROM {project_issue_subscriptions_user} WHERE uid = %d", array($account->uid))); + $settings = ($settings ? $settings : array()); + $account->project_issue_subscriptions = array_merge($defaults, $settings); + } + // Otherwise, attach default settings. + else { + $account->project_issue_subscriptions = $defaults; + } +} + +/** + * Saves user account settings for project issue subscriptions. + * + * @param $account + * A user account object containing at least the properties: + * - uid: The ID of the user account. + * - project_issue_subscriptions: An associative array containing: + * - level: An integer denoting the user's global issue subscription level. + * + * @see project_issue_subscriptions_user_settings_load() + */ +function project_issue_subscriptions_user_settings_save($account) { + db_query("UPDATE {project_issue_subscriptions_user} SET level = %d WHERE uid = %d", array( + $account->project_issue_subscriptions['level'], + $account->uid, + )); + if (!db_affected_rows()) { + db_query("INSERT INTO {project_issue_subscriptions_user} (uid, level) VALUES (%d, %d)", array( + $account->uid, + $account->project_issue_subscriptions['level'], + )); + } +} + +/** + * Loads the project subscription setting for a user account. + * + * @param $uid + * The ID of a user account. + * @param $nid + * The node ID of a project node. + * + * @return + * An integer denoting the user's project issue subscription level: + * - PROJECT_ISSUE_SUBSCRIPTIONS_NONE + * - PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED + * - PROJECT_ISSUE_SUBSCRIPTIONS_ALL + * + * @see project_issue_subscriptions_project_setting_save() + */ +function project_issue_subscriptions_project_setting_load($uid, $nid) { + $level = db_result(db_query("SELECT level FROM {project_issue_subscriptions_project} WHERE uid = %d AND nid = %d", array( + $uid, + $nid, + ))); + return ($level !== FALSE ? $level : PROJECT_ISSUE_SUBSCRIPTIONS_NONE); +} + +/** + * Saves project subscription setting for a user account. + * + * @param $uid + * The ID of a user account. + * @param $nid + * The node ID of a project node. + * @param $level + * An integer denoting the user's project issue subscription level: + * - PROJECT_ISSUE_SUBSCRIPTIONS_NONE + * - PROJECT_ISSUE_SUBSCRIPTIONS_FLAGGED + * - PROJECT_ISSUE_SUBSCRIPTIONS_ALL + * + * @see project_issue_subscriptions_project_setting_load() + */ +function project_issue_subscriptions_project_setting_save($uid, $nid, $level) { + db_query("UPDATE {project_issue_subscriptions_project} SET level = %d WHERE uid = %d AND nid = %d", array( + $level, + $uid, + $nid, + )); + if (!db_affected_rows()) { + db_query("INSERT INTO {project_issue_subscriptions_project} (uid, nid, level) VALUES (%d, %d, %d)", array( + $uid, + $nid, + $level, + )); + } +} + +/** + * Deletes a project subscription setting of a user account. + * + * @param $uid + * The ID of a user account. + * @param $nid + * The node ID of a project node. + * + * @see project_issue_subscriptions_project_setting_load() + */ +function project_issue_subscriptions_project_setting_delete($uid, $nid) { + db_query("DELETE FROM {project_issue_subscriptions_project} WHERE uid = %d AND nid = %d", array( + $uid, + $nid, + )); +} + +/** + * Implements hook_user(). + */ +function project_issue_user($op, $edit, $account) { + if ($op == 'delete') { + db_query("DELETE FROM {project_issue_subscriptions_user} WHERE uid = %d", $account->uid); + db_query("DELETE FROM {project_issue_subscriptions_project} WHERE uid = %d", $account->uid); + db_query("DELETE FROM {project_issue_project_maintainer} WHERE uid = %d", $account->uid); + } +} + +/** * hook_nodeapi() implementation. This just decides what type of node * is being passed, and calls the appropriate type-specific hook. * @@ -1852,13 +1995,6 @@ function project_issue_query_result_links($project_arg = NULL) { '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. @@ -2204,187 +2340,3 @@ function project_issue_project_promote_sandbox($project) { /** * @} End of "defgroup project_issue_solr". */ - -/** - * Implementation of hook_widget_info(). - */ -function project_issue_widget_info() { - return array( - // Widget key is limited to 32 chars. - 'project_issue_nodereference_auto' => array( - 'label' => t('Project issue autocomplete text field'), - 'field types' => array('nodereference'), - 'multiple values' => CONTENT_HANDLE_CORE, - 'callbacks' => array( - 'default value' => CONTENT_CALLBACK_DEFAULT, - ), - ), - ); -} - -/** - * Implementation of hook_widget(). - */ -function project_issue_widget(&$form, &$form_state, $field, $items, $delta = 0) { - switch ($field['widget']['type']) { - case 'project_issue_nodereference_auto': - // Get the default process function from hook_elements. - $default_process = (($info = _element_info('nodereference_autocomplete')) && array_key_exists('#process', $info)) ? $info['#process'] : array(); - // Add our new process element to the default. - $default_process[] = 'project_issue_nodereference_autocomplete_process'; - return array( - '#type' => 'nodereference_autocomplete', - '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, - '#value_callback' => 'project_issue_nodereference_autocomplete_value', - '#process' => $default_process, - ); - } -} - -/** - * Returns the value for a project_issue_nodereference autocomplete widget. - * - * Finds the node title from the node ID, and returns a value containing both. - * - * @see nodereference_autocomplete_value() - */ -function project_issue_nodereference_autocomplete_value($element, $edit = FALSE) { - $field_key = $element['#columns'][0]; - if (!empty($element['#default_value'][$field_key])) { - $nid = (int) $element['#default_value'][$field_key]; - $value = '#'. $nid .': '. db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $nid)); - return array($field_key => $value); - } - return array($field_key => NULL); -} - -/** - * Sets the validation and auto-complete path for the node reference widget. - * - * This is an additional process function for the - * project_issue_nodereference_autocomplete widget. It runs after the - * default, nodereference_autocomplete_process(). - */ -function project_issue_nodereference_autocomplete_process($element, $edit, $form_state, $form) { - // Get the field key, since the autocomplete element defined by nodereference - // module just wraps a textfield. - $field_key = $element['#columns'][0]; - - // Add our custom autocomplete callback. - $element[$field_key]['#autocomplete_path'] = 'project/autocomplete/issues/nodereference'; - - // Unset the default validate. - $key = array_search('nodereference_autocomplete_validate', $element[$field_key]['#element_validate']); - if ($key !== FALSE) { - unset($element[$field_key]['#element_validate'][$key]); - } - - // Add our custom validate callback. - $element[$field_key]['#element_validate'][] = 'project_issue_nodereference_autocomplete_validate'; - - return $element; -} - -/** - * Validates a project_issue_nodereference_autocomplete element. - * - * The field has valid input if it contains a node ID, a node title, or the - * format supplied by the auto-complete function ('#NID: TITLE'), and if the - * node is a project issue. - */ -function project_issue_nodereference_autocomplete_validate($element, &$form_state) { - // Get field information from $element. - $field_name = $element['#field_name']; - $type_name = $element['#type_name']; - $field = content_fields($field_name, $type_name); - $field_key = $element['#columns'][0]; - $value = $element['#value'][$field_key]; - $nid = NULL; - $title = ''; - - // Sanity check: we expect $value to be a string or number, so just make sure. - if (!is_string($value) && !is_numeric($value)) { - form_error($element[$field_key], t('%name: the value is not a valid issue title or node ID.', array('%name' => $field['widget']['label']))); - return; - } - - $value = trim($value); - - // Try to pick the node ID or title out of the information we have. - // First try to match with auto-complete syntax ('#NID: TITLE') (we will be - // ignoring the title, however). Make sure we have at least one non-zero - // digit at the start of the NID. - if (preg_match('/^#([1-9]\d*):/', $value, $matches)) { - $nid = $matches[1]; - } - // Autocomplete syntax didn't work, so see if we have a node ID. - elseif (is_numeric($value)) { - $nid = (int) $value; - if ($nid <= 0) { - $nid = NULL; - form_error($element[$field_key], t('%name: Not a valid issue ID.', array('%name' => $field['widget']['label']))); - } - } - // As a last resort, it could be a node title. - else { - $title = $value; - } - - // Now validate either the node ID or the title, making sure it's a - // published issue node. - if (!empty($nid)) { - if (!$nid = db_result(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.type = 'project_issue' AND n.nid = %d AND status = 1"), $nid))) { - form_error($element[$field_key], t('%name: Not a valid issue ID.', array('%name' => $field['widget']['label']))); - } - } - else if (!empty($title)) { - if (!$nid = db_result(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.type = 'project_issue' AND n.status = 1 AND n.title = '%s'"), $title))) { - form_error($element[$field_key], t('%name: the value is not a valid issue ID or title.', array('%name' => $field['widget']['label']))); - } - } - - form_set_value($element, $nid, $form_state); -} - -/** - * Implements hook_field_formatter_info(). - * - * Provides a formatter for nodereference fields that uses - * theme_project_issue_issue_link(). - */ -function project_issue_field_formatter_info() { - return array( - 'issue_id' => array( - 'label' => t('Issue link styled with status metadata'), - 'field types' => array('nodereference'), - ), - 'issue_id_assigned' => array( - 'label' => t('Issue link styled with status metadata and assignee'), - 'field types' => array('nodereference'), - ), - ); -} - -/** - * Themes a node reference field with status information. - */ -function theme_project_issue_formatter_issue_id($element) { - $project_issue_node = node_load($element['#item']['nid']); - if (!$project_issue_node || $project_issue_node->type != "project_issue") { - // The value here isn't an issue node, so just return the plain value. - return check_plain($element['#item']['nid']); - } - return theme('project_issue_issue_link', $project_issue_node); -} - -/** - * Themes a node reference field with status and assignee information. - */ -function theme_project_issue_formatter_issue_id_assigned($element) { - $project_issue_node = node_load($element['#item']['nid']); - if (!$project_issue_node || $project_issue_node->type != "project_issue") { - // The value here isn't an issue node, so just return the plain value. - return check_plain($element['#item']['nid']); - } - return theme('project_issue_issue_link', $project_issue_node, NULL, NULL, TRUE); -}