'Autocomplete tql query', 'page callback' => 'tql_autocomplete', 'access callback' => 'user_access', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK ); $items['admin/settings/tql'] = array( 'title' => 'Taxonomy Query Language', 'description' => 'Modify the behavior of the TQL module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('tql_admin_settings'), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'type' => MENU_NORMAL_ITEM ); return $items; } /** * Menu callback for administration settings. */ function tql_admin_settings() { $form = array(); $form['tql_dump_query'] = array( '#type' => 'checkbox', '#title' => t('Dump the query as a drupal message'), '#description' => t('Setting this will set a message showing the query in the "message" area of the page.'), '#return_value' => TRUE, '#default_value' => variable_get('tql_dump_query', TRUE) ); return system_settings_form($form); } /** * Implementation of hook_search(). * This creates a tab on the search page to execute a query. */ function tql_search($op = 'search', $keys = NULL) { switch ($op) { case 'name': // Name of tab for search page return t('Taxonomy'); case 'search': if ($keys && $nid_sql = tql_generate_sql($keys)) { $search_results = array(); // Create SQL for nids and number of results $sql = 'SELECT nid FROM {node} WHERE status = 1 AND nid in ('. $nid_sql .')'; $count_sql = 'SELECT COUNT(*) FROM {node} WHERE status = 1 AND nid in ('. $nid_sql .')'; // Let 'pager' execute the query and make nice 'previous', 'next' links $result = pager_query($sql, 10, 0, $count_sql, $keys); while ($node = db_fetch_object($result)) { // Load node $node = node_load($node->nid); // Generate snippet $node->snippet = check_markup($node->teaser, $node->format, FALSE); $terms = taxonomy_link('taxonomy terms', $node); $node->snippet .= theme('links', $terms); // Let other modules add extra stuff to search result $extra = node_invoke_nodeapi($node, 'search result'); // Construct search result entry $search_results[] = array( 'link' => url('node/'.$node->nid, array('absolute' => TRUE)), 'title' => $node->title, 'type' => node_get_types('name', $node), 'user' => theme('username', $node), 'date' => $node->changed, 'extra' => $extra, 'snippet' => $node->snippet ); } return $search_results; } break; } } /** * Function to run a TQL string through the parser to retrieve the matching nid's. * This function is intended to be used by other modules. Callers are expected * to load the nodes themselves to extract the data they need. * * @param String $query Query string, possibly enhanced with TQL keywords. */ function tql_get_nids($query) { $matched_nids = array(); if (!empty($query)) { $nid_sql = tql_generate_sql($query); if ($nid_sql != null) { // Create SQL for nids and number of results $sql = 'SELECT nid FROM {node} WHERE status = 1 AND nid in ('. $nid_sql .')'; $count_sql = 'SELECT COUNT(*) FROM {node} WHERE status = 1 AND nid in ('. $nid_sql .')'; // Let 'pager' execute the query and make nice 'previous', 'next' links $result = pager_query($sql, 10, 0, $count_sql, $keys); while ($node = db_fetch_object($result)) { $matched_nids[] = $node->nid; } } } return $matched_nids; } /** * Implementation of hook_form_alter(). */ function tql_form_alter(&$form, $form_state, $form_id) { if ($form_id == 'search_form' && arg(1) == 'tql') { $form['basic']['#title'] = t('Enter your query'); $form['basic']['inline']['keys']['#autocomplete_path'] = 'tql/autocomplete/0'; } } /** * Implementation of hook_views_tables(). * Provide information for views module. */ function tql_views_tables() { if (module_exists('taxonomy')) { // Views filter for a query over all vocabularies. $tables["tql_node"] = array( 'name' => 'node', // {node} table is used 'provider' => 'internal', 'filters' => array( 'tid' => array( 'name' => t('Taxonomy: Query over all Vocabularies'), 'value' => array( '#type' => 'textfield', '#autocomplete_path' => 'tql/autocomplete/0', ), 'option' => 'string', 'operator' => array('Has'), 'handler' => 'tql_handler_filter_query', 'help' => t('All terms from every vocabulary will be used to check against the specified query. This filter is not aware of taxonomy hierarchies. Please see the taxonomy help for more information.'), ), ), ); $vocabularies = taxonomy_get_vocabularies(); foreach ($vocabularies as $voc) { // Views filter for a query over a single vocabulary $tables["tql_node_$voc->vid"] = array( 'name' => 'node', 'provider' => 'internal', 'filters' => array( 'tid' => array( 'name' => t('Taxonomy: Query for @voc-name', array('@voc-name' => $voc->name)), 'value' => array( '#type' => 'textfield', '#autocomplete_path' => 'tql/autocomplete/'. $voc->vid, ), 'option' => 'string', 'operator' => array('Has'), 'handler' => 'tql_handler_filter_query', 'vocabulary' => $voc->vid, 'help' => t("Only terms associated with %voc-name will be used to check against the specified query. This filter is not aware of taxonomy hierarchies. Please see the taxonomy help for more information.", array('%voc-name' => $voc->name)), ), ) ); } } return $tables; } /** * Implementation of hook_views_arguments(). * Provide information for views module. */ function tql_views_arguments() { $arguments = array( 'tql_query' => array( 'name' => t('Taxonomy: Query'), 'handler' => 'tql_handler_arg_query', 'option' => 'string', 'help' => t('Todo: query, named query. Options can be set to the name of an exposed Taxonomy: Query filter to insert the argument into this field.'), ) ); return $arguments; } /** * Callback when query filter is used as a views argument. */ function tql_handler_arg_query($op, &$query, $argtype, $arg = NULL) { switch ($op) { case 'summary': $query->ensure_table('term_data', TRUE); $query->add_field('name', 'term_data'); $query->add_field('weight', 'term_data'); $query->add_field('tid', 'term_data'); $fieldinfo['field'] = "term_data.name"; return $fieldinfo; case 'sort': $query->add_orderby('node', 'title', $argtype); break; case 'filter': // query or query name is in $arg // execute query or stored query and apply this to $query $filter = array('value' => $arg); $filterinfo = array('table' => 'node'); tql_handler_filter_query('', $filter, $filterinfo, $query); // If option field is set, we store the query as a $_GET variable. // That way it can be inserted into an exposed field if (isset($argtype['options'])) { $_GET[$argtype['options']] = $arg; } break; case 'link': break; case 'title': // query or query name is in $query // return title $title = $query; return $title; } } /** * Callback when filter has to construct SQL query for views. */ function tql_handler_filter_query($op, $filter, $filterinfo, &$query) { if (isset($filterinfo['vocabulary'])) { $sql = tql_generate_sql($filter['value'], array($filterinfo['vocabulary'])); } else { $sql = tql_generate_sql($filter['value']); } if ($sql) { $table = $filterinfo['table']; $query->ensure_table($table); $query->add_where("node.nid in (". $sql .")"); } else { $query->add_where("FALSE"); } } /** * Generate an SQL query of a tag query. * * @param $query * The query which should be parsed * @param $vocabulary_list * An array of vocabulary IDs if the query should be restricted to these vocabularies. * @return * The generated SQL is a select query over a list of node ids and can be used * in a where clause as 'WHERE node.nid in ($sql_query)'. */ function tql_generate_sql($query, $vocabulary_list = NULL) { $sql = NULL; // Include needed files include_once('ast/TqlAbstractAstVisitor.php'); include_once('ast/TqlAstDumper.php'); include_once('ast/TqlErrorFormatter.php'); include_once('ast/TqlNameToTid.php'); include_once('ast/TqlMySqlGenerator.php'); include_once('ast/TqlParser.php'); include_once('ast/TqlLexer.php'); // init the lexer with the query $lexer = new TqlLexer($query); $parser = new TqlParser(); // build the AST while ($lexer->yylex()) { $parser->doParse($lexer->token, $lexer->node); } $parser->doParse(0, 0); if ($parser->successful) { // map terms (strings) to term ids (integers) $tag_tank = tql_create_tag_tank($parser->ast, $vocabulary_list); // generate code for mysql $sql_generator = new TqlMySqlGenerator(); $sql = $sql_generator->generate($parser->ast, $tag_tank->terms, '{term_node}'); if (variable_get('tql_dump_query', TRUE)) { // write the query nicely formatted $dumper = new TqlAstDumper(); // drupal_set_message($dumper->dump($parser->ast)); } } else { tql_create_tag_tank($parser->astParts, $vocabulary_list); $errorPrinter = new TqlErrorFormatter(); $error = $errorPrinter->error($parser->astParts, $query); drupal_set_message(t("Error in query: ") . $error, 'error'); } return $sql; } /** * Creates a TqlNameToTid object and reports missing terms to drupal. * * @param $ast * Object of type TqlNode or an array of such objects. * @param $vocabulary_list * Array of vocabulary IDs to use or 'null' if all should be taken into account. * @return * TqlNameToId Object */ function tql_create_tag_tank($ast, $vocabulary_list) { $tag_tank = new TqlNameToTid(); if (is_array($ast)) { foreach ($ast as $part_of_ast) { // map terms (strings) to term ids (integers) $tag_tank->computeTermIDs($part_of_ast, $vocabulary_list); } } else { $tag_tank->computeTermIDs($ast, $vocabulary_list); } // report terms which do not occur in the selected vocabularies at all if (count($tag_tank->missingTerms) > 0) { drupal_set_message(t("The following terms were not found: ") . implode(', ', $tag_tank->missingTerms), 'error'); } return $tag_tank; } /** * Menu callback to autocomplete a query. * * @param $vid * The vocabulary to restrict the autocompletion. If this is zero, no restriction is used. * @param $string * The query for which autocompletion should be done. */ function tql_autocomplete($vid = 0, $string = '') { // Include needed files include_once('ast/TqlAbstractAstVisitor.php'); include_once('ast/TqlParser.php'); include_once('ast/TqlLexer.php'); // Remove unneeded whitespace $string = trim($string); // initialize the lexer with the query $lexer = new TqlLexer($string); // Get last token $last_token = NULL; while ($lexer->yylex()) { $last_token = $lexer->token; $last_node = $lexer->node; } // Only autocomplete identifiers if ($last_token == TqlParser::TK_IDENTIFIER) { // Restrict terms to vocabulary if requested if ($vid) { $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_node->value, 0, 10); } else { $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $last_node->value, 0, 10); } // Base part of autocompleted string $prefix = drupal_substr($string, 0, $last_node->column); // Check if a quote is left of the last token $has_left_quote = ($last_node->column - 1 >= 0) ? $string[$last_node->column - 1] == '"' : FALSE; // Construct response array $matches = array(); while ($tag = db_fetch_object($result)) { $name = $tag->name; if (strpos($name, ' ') !== FALSE) { // the term needs to be in quotes if ($has_left_quote) { $name .= '"'; } else { $name = '"'. $name .'"'; } } elseif ($has_left_quote) { // a quote is already there, close it $name .= '"'; } // Array key is for textfield, array value for display $matches[$prefix . $name] = check_plain($tag->name); } // Return matches as javascript array print(drupal_to_js($matches)); exit(); } else { exit(); } }