From e1616ef886bcc01a04396ff27e897b5c1bf66329 Wed, 6 Jul 2011 17:42:28 -0700
From: Jennifer Hodgdon <yahgrp@poplarware.com>
Date: Wed, 6 Jul 2011 17:41:37 -0700
Subject: [PATCH] Incorporate webchick's comments to previous patch by mikey_p

diff --git a/includes/autocomplete.inc b/includes/autocomplete.inc
index fd15366..934daae 100644
--- a/includes/autocomplete.inc
+++ b/includes/autocomplete.inc
@@ -71,3 +71,76 @@
   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/project_issue.module b/project_issue.module
index 4345d1d..6ab85d2 100644
--- a/project_issue.module
+++ b/project_issue.module
@@ -217,6 +217,16 @@
     '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;
 }
 
@@ -378,6 +388,14 @@
       ),
       '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',
     ),
   );
 }
@@ -2186,3 +2204,177 @@
 /**
  * @} 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';
+      $element = array(
+        '#type' => 'nodereference_autocomplete',
+        '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL,
+        '#value_callback' => 'project_issue_nodereference_autocomplete_value',
+        '#process' => $default_process,
+      );
+      break;
+  }
+  return $element;
+}
+
+/**
+ * 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() for reference.
+ */
+function project_issue_nodereference_autocomplete_value($element, $edit = FALSE) {
+  $field_key = $element['#columns'][0];
+  if (!empty($element['#default_value'][$field_key])) {
+    $nid = intval($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.
+  foreach ($element[$field_key]['#element_validate'] as $key => $value) {
+    if ($value == 'nodereference_autocomplete_validate') {
+      unset($element[$field_key]['#element_validate'][$key]);
+    }
+  }
+
+  // Set our custom validate callback.
+  array_unshift($element[$field_key]['#element_validate'], 'project_issue_nodereference_autocomplete_validate');
+
+  return $element;
+}
+
+/**
+ * Validates a project_issue_nodereference_autocomplete element.
+ */
+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;
+  // See if the user has entered a value, which should have been provided
+  // by the autocomplete callback in the form '#NID: TITLE', or else they might
+  // have just typed in a node ID or a title.
+  if (!empty($value)) {
+    // First try to match with auto-complete syntax.
+    preg_match('/^#(\d+):\s*(.*)$/', $value, $matches);
+    if (!empty($matches)) {
+      // Find the node and verify that the title matches, and that it's the
+      // right node type.
+      list(, $nid, $title) = $matches;
+      $n = node_load($nid);
+      if (!$n || $n->type != 'project_issue') {
+          form_error($element[$field_key], t('%name: the value is not a valid issue ID.', array('%name' => t($field['widget']['label']))));
+      }
+      else if (!empty($title) && $n && trim($title) != trim($n->title)) {
+        form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
+      }
+    }
+    else {
+      // Autocomplete syntax didn't work, so try to match a node ID.
+      if (is_numeric($value)) {
+        if (!$nid = db_result(db_query("SELECT n.nid FROM {node} n WHERE n.type = 'project_issue' AND n.nid = %d", $value))) {
+          form_error($element[$field_key], t('%name: the value is not a valid issue ID.', array('%name' => t($field['widget']['label']))));
+        }
+      }
+      // That didn't work either, so try matching just a node title.
+      elseif (is_string($value)) {
+        if (!$nid = db_result(db_query("SELECT n.nid FROM {node} n WHERE n.type = 'project_issue' AND n.status = 1 AND n.title = '%s'", trim($value)))) {
+          form_error($element[$field_key], t('%name: the value is not a valid issue title.', array('%name' => t($field['widget']['label']))));
+        }
+      }
+      else {
+        // Sanity check: we don't expect to get anything but numbers and
+        // strings here, but just in case, throw an error as it's not valid.
+        form_error($element[$field_key], t('%name: the value is not a valid issue title or node ID.', array('%name' => t($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") {
+    return $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") {
+    return $element['#item']['nid'];
+  }
+  return theme('project_issue_issue_link', $project_issue_node, NULL, NULL, TRUE);
+}
