'Project summary', 'description' => '', 'page callback' => 'project_solr_browse_summary_page', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['project/%'] = array( 'title' => 'Project summary', 'description' => '', 'page callback' => 'project_solr_browse_page', 'page arguments' => array(1), 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['project/%project_type/categories'] = array( 'title' => 'Categories', 'page callback' => 'project_solr_category_page', 'page arguments' => array(1), 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); return $items; } /** * Implementation of hook_theme(). */ function project_solr_theme() { return array( 'project_solr_no_count_facet_link' => array( 'arguments' => array( 'facet_text' => NULL, 'path' => '', 'options' => '', 'active' => FALSE, 'num_found' => NULL, ), ), 'project_solr_category_page' => array( 'arguments' => array( 'project_type' => NULL, 'categories' => array(), 'version_form' => NULL, ), ), 'project_solr_category_list' => array( 'arguments' => array( 'items' => array(), ), ), ); } /** * Implementation of hook_nodeapi(). * * Whenever a release node is edited or submitted, if the node is now * published, reindex the project node associated with that release. */ function project_solr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { switch ($op) { case 'update': case 'insert': if ($node->type == 'project_release' && $node->status) { apachesolr_mark_node($node->project_release['pid']); } break; } } /** * Implementation of hook_form_alter(). */ function project_solr_form_apachesolr_delete_index_form_alter(&$form, $form_state) { $form['reindex_project'] = array( '#type' => 'submit', '#value' => t('Re-index all projects'), '#submit' => array('project_solr_reindex_projects'), ); $form['reindex_project_desc'] = array( '#type' => 'item', '#description' => t('This will only re-index the project content on your site.'), ); } function project_solr_reindex_projects($form, $form_state) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = 'project_project')", time()); drupal_set_message(t('Marked all project content to be reindexed by Apache Solr.')); } //---------------------------------------- // Solr-related hooks //---------------------------------------- /** * Implementation of hook_apachesolr_facets(). */ function project_solr_apachesolr_facets() { $facets = array(); if (module_exists('project_usage')) { $facets = array( 'sort_most_installed' => array( 'info' => t('Most installed'), 'facet_field' => 'sis_project_release_usage', 'direction' => 'desc', ), ); } return $facets; } /** * Implementation of hook_apachesolr_update_index(). * * This adds information about releases for the project to the Solr document * so we can facet on releases (API compatibility terms, usage, etc), along * with other project-specific metadata (e.g. shortname/uri). * * Beware that this hook is invoked for all nodes, so we should be careful in * here to check that we're really dealing with a project node before trying * to access any project-specifc data. */ function project_solr_apachesolr_update_index(&$document, $node) { if (!empty($node->project['uri'])) { $document->ss_project_uri = $node->project['uri']; } if (!empty($node->project)) { // The initial "bs" is for boolean/single. $document->bs_project_sandbox = !empty($node->project['sandbox']) ? '1' : '0'; } if (module_exists('project_release') && !empty($node->project_release['releases'])) { $document->bs_project_has_releases = '1'; $max_filetime = 0; $max_official_filetime = 0; $term_query = db_query("SELECT DISTINCT(tn.tid) FROM {node} n INNER JOIN {project_release_nodes} prn ON n.nid = prn.nid INNER JOIN {term_node} tn ON n.nid = tn.nid INNER JOIN {term_data} td ON tn.tid = td.tid WHERE prn.pid = %d AND td.vid = %d", $node->nid, _project_release_get_api_vid()); while ($term = db_fetch_object($term_query)) { $document->setMultiValue('im_project_release_api_tids', $term->tid); $latest_activity = db_fetch_object(db_query_range("SELECT f.timestamp, prn.rebuild FROM {node} n INNER JOIN {project_release_nodes} prn ON n.nid = prn.nid INNER JOIN {project_release_file} prf ON prn.nid = prf.nid INNER JOIN {term_node} tn ON prn.nid = tn.nid INNER JOIN {files} f ON prf.fid = f.fid WHERE n.status = 1 AND tn.tid = %d AND prn.pid = %d ORDER BY f.timestamp DESC", $term->tid, $node->nid, 0, 1)); $filetime = $latest_activity->timestamp; $key = 'ds_project_latest_activity_'. $term->tid; $document->$key = apachesolr_date_iso($filetime); if ($filetime > $max_filetime) { $max_filetime = $filetime; } // Now, look for the most recent official release for this API version. $key = 'ds_project_latest_release_'. $term->tid; if ($latest_activity->rebuild == 0) { // The latest activity is official, we're done. $document->$key = apachesolr_date_iso($filetime); if ($filetime > $max_official_filetime) { $max_official_filetime = $filetime; } } else { $filetime = db_result(db_query_range("SELECT f.timestamp FROM {node} n INNER JOIN {project_release_nodes} prn ON n.nid = prn.nid INNER JOIN {project_release_file} prf ON prn.nid = prf.nid INNER JOIN {term_node} tn ON prn.nid = tn.nid INNER JOIN {files} f ON prf.fid = f.fid WHERE n.status = 1 AND prn.rebuild = 0 AND tn.tid = %d AND prn.pid = %d ORDER BY f.timestamp DESC", $term->tid, $node->nid, 0, 1)); if (!empty($filetime)) { $document->$key = apachesolr_date_iso($filetime); } if ($filetime > $max_official_filetime) { $max_official_filetime = $filetime; } } } // Set Latest Activity to be the last file update time for the newest release on the project. $document->ds_project_latest_activity = apachesolr_date_iso($max_filetime); if (!empty($max_official_filetime)) { // Set Latest Release to be the last file update time for an official release on the project. $document->ds_project_latest_release = apachesolr_date_iso($max_official_filetime); } if (module_exists('project_usage')) { $weeks = variable_get('project_usage_active_weeks', array()); $week = reset($weeks); $total_usage = 0; $query = db_query("SELECT * FROM {project_usage_week_project} WHERE nid = %d AND timestamp = %d", $node->nid, $week); while ($usage = db_fetch_object($query)) { $key = 'sis_project_release_usage_'. $usage->tid; $document->$key = $usage->count; $total_usage += $usage->count; } $document->sis_project_release_usage = $total_usage; } } } /** * Implementation of hook_apachesolr_modify_query(). * * Extract all of the project-type-related filters, which will be present * in the url in the format im_vid_X:Y im_vid_X:Z, and turn them into a * relevant subquery of the format (im_vid_X:Y OR im_vid_X:Z). This is done to * allow searching for results matching any of Y or Z inside of vocabulary X. * e.g. Return all themes that are Fixed OR Fluid. */ function project_solr_apachesolr_modify_query(&$query, &$params) { $filter_set = array(); // Grab vids for all project-type-related vocabularies $vocabs = project_get_related_tids_map(TRUE); $project_type_related_vids = array_keys($vocabs); foreach ($query->get_filters() as $filter) { // Grab any project-type-related filters (im_vid_X filters). if (preg_match('/^im_vid_(.*)$/', $filter['#name'], $matches)) { if (in_array($matches[1], $project_type_related_vids)) { $filter_set[$filter['#name']][] = $filter['#value']; $query->remove_filter($filter['#name'], $filter['#value']); } } } // Add the project-type-related filters back as an OR list. foreach ($filter_set as $name => $filters) { $tid_query = apachesolr_drupal_query(); foreach ($filters as $value) { $tid_query->add_filter($name, $value); } $query->add_subquery($tid_query, 'OR'); } // If we don't have any explicit filter for the sandbox status projects, // add an explicit exclusion for sandbox projects or their related content // (e.g. issues from sandbox projects). We want this for all solr queries. if (!$query->get_filters('bs_project_sandbox')) { // If not defined, we want to *hide* sandboxes (to match the // #default_value for the exposed filter), hence 1, with the third // 'exclude' parameter set to TRUE (inverts the sense of the filter). $query->add_filter('bs_project_sandbox', '1', TRUE); // As we're building the query for the project browsing pages, we try to // add this filter to hide projects without releases, but only if the // query already has a filter that hides sandboxes. The default filter // above to exclude sandbox content won't have been added yet, so we won't // have the filter for releases, either, unless we add it here ourselves // (but only if we're building a query for project nodes). if ($query->has_filter('type', 'project_project') && module_exists('project_release')) { $query->add_filter('bs_project_has_releases', '1'); } } } /** * Implementation of hook_apachesolr_prepare_query(). * * This just adds an alias to allow the im_project_release_api_tids to present * itself as something different in the url. In the default case, instead of * im_project_release_api_tids displaying in the url, api_version will display * in the url, making the url more user-friendly. */ function project_solr_apachesolr_prepare_query(&$query, &$params) { if (module_exists('project_release')) { // Add a human-readable alias for the release filter. $query->add_field_aliases(array('im_project_release_api_tids' => variable_get('project_solr_project_release_api_tids_alias', 'api_version'))); } // Only add the project-specific sort if the query is filtering on projects. if ($query->has_filter('type', 'project_project')) { project_solr_add_sorts($query); } } //---------------------------------------- // Page callbacks //---------------------------------------- /** * Summary project browsing page. */ function project_solr_browse_summary_page() { $items = array(); $project_types = project_get_project_types(); foreach ($project_types as $term) { $items[] = theme('project_type', $term); } drupal_set_title(t('Project types')); return theme('item_list', $items); } function project_solr_browse_page($term_name) { try { $output = ''; $project_type = db_fetch_object(db_query("SELECT t.tid, t.name, t.description FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) = LOWER('%s')", _project_get_vid(), $term_name)); if (!$project_type) { // XXX: this is the Drupal 5 way... return drupal_not_found(); } drupal_set_title(check_plain($project_type->name)); if (!empty($project_type->description)) { $output .= theme('project_type_description', $project_type); } $text_query = isset($_GET['text']) ? $_GET['text'] : ''; $filters = isset($_GET['filters']) ? $_GET['filters'] : ''; $sort = isset($_GET['solrsort']) ? check_plain($_GET['solrsort']) : ''; // Validate sort parameter. if ((!isset($sort) || !preg_match('/^([a-z0-9_]+ (asc|desc)(,)?)+$/i', $sort)) && empty($text_query)) { $sort = variable_get('project_solr_default_sort', 'sort_title asc'); } include_once drupal_get_path('module', 'project_solr') .'/ProjectSolrQuery.php'; $query = new ProjectSolrQuery(apachesolr_get_solr(), $text_query, $filters, $sort, 'project/' . drupal_strtolower($project_type->name)); if (is_null($query)) { throw new Exception(t('Could not construct a Solr query.')); } $params = array( 'fl' => 'id,nid,title,body,format,comment_count,type,created,changed,score,url,uid,name,sis_project_release_usage,ds_project_latest_release,ds_project_latest_activity', 'rows' => variable_get('apachesolr_rows', 10), 'facet' => 'true', 'facet.mincount' => 1, 'facet.sort' => 'true', 'facet.field' => array( 'im_vid_'. _project_get_vid(), 'im_project_release_api_tids', ), 'facet.limit' => 200, ); $page = isset($_GET['page']) ? $_GET['page'] : 0; $params['start'] = $page * $params['rows']; // This is the object that does the communication with the solr server. $solr = apachesolr_get_solr(); // We add add_filter() parameters here to include all the constant // filters for the query -- project nodes of the given top-level type that // have releases (if project_release is enabled). // We use add_filter() rather than $params['fq'] so that our filters // are correctly passed to anything that uses our cached query. $query->add_filter('type', 'project_project'); $query->add_filter('im_vid_'. _project_get_vid(), $project_type->tid); // We can only filter on bs_project_has_releases for just official projects, // since sandbox projects can never have releases. if (module_exists('project_release') && $query->has_filter('bs_project_sandbox', '0')) { $query->add_filter('bs_project_has_releases', '1'); } // Allow modules to alter the query prior to statically caching it. // This can e.g. be used to add available sorts. foreach (module_implements('apachesolr_prepare_query') as $module) { $function_name = $module . '_apachesolr_prepare_query'; $function_name($query, $params, 'project_solr_browse_page'); } // Cache the built query. Since all the built queries go through // this process, all the hook_invocations will happen later. apachesolr_current_query($query); // This hook allows modules to modify the query and params objects. apachesolr_modify_query($query, $params, 'project_solr'); if (!$query) { return array(); } // Force sort to be by the corresponding core compatibility if filtered. $sort = $query->get_solrsort(); if (in_array($sort['#name'], array('ds_project_latest_release', 'ds_project_latest_activity')) && ($api_filters = $query->get_filters('im_project_release_api_tids'))) { $first_filter = reset($api_filters); $params['sort'] = $sort['#name'] .'_'. $first_filter['#value'] .' '. $sort['#direction']; } $response = $solr->search($query->get_query_basic(), $params['start'], $params['rows'], $params); // The response is cached so that it is accessible to the blocks and anything // else that needs it beyond the initial search. $total = $response->response->numFound; apachesolr_static_response_cache($response); apachesolr_has_searched(TRUE); // Set breadcrumb. $breadcrumb = menu_get_active_breadcrumb(); drupal_set_breadcrumb($breadcrumb); $output .= '
'; pager_query("SELECT %d", $params['rows'], 0, NULL, $total); if ($total > 0) { foreach ($response->response->docs as $doc) { $doc->created = strtotime($doc->created); $doc->changed = strtotime($doc->changed); $output .= project_solr_render_search_result($doc); } } else { $output .= t('No projects found in this category.'); } $output .= '
'; // id="project-overview" $output .= theme('pager', NULL, $params['rows'], 0); $output = drupal_get_form('project_solr_browse_projects_form', $project_type, 'project/' . drupal_strtolower($project_type->name)) . $output; } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR); apachesolr_failure(t('Solr search'), is_null($query) ? $keys : $query->get_query_basic()); } return $output; } //---------------------------------------- // Blocks //---------------------------------------- /** * Implementation of hook_block(). * * @see project_solr_block_info() * @see project_solr_block_view() */ function project_solr_block($op = 'list', $delta = 0, $edit = array()) { // Instead of a monolithic function, split out this code into separate // functions for each operation (which we're going to need when we port to // D7, anyway). switch ($op) { case 'list': return project_solr_block_info(); case 'view': return project_solr_block_view($delta); } } /** * Declare all the blocks provided by project_solr. * * @see project_solr_block() */ function project_solr_block_info() { $blocks = array( 'project_solr_categories' => array( 'info' => t('Project Solr: categories'), 'cache' => BLOCK_CACHE_PER_PAGE, ), 'project_solr_text' => array( 'info' => t('Project Solr: text search'), 'cache' => BLOCK_CACHE_PER_PAGE, ), ); if (module_exists('project_release')) { $blocks['project_solr_compability'] = array( 'info' => t('Project Solr: core compatibility'), 'cache' => BLOCK_CACHE_PER_PAGE, ); } return $blocks; } /** * Render the content for the requested project_solr block. * * @param $delta * The block delta for the block to render (the key from the array returned * by project_solr_block_info()). * * @see project_solr_block() * @see project_solr_block_info() */ function project_solr_block_view($delta) { // None of these blocks make sense unless there's been a solr search. if (!apachesolr_has_searched()) { return; } $response = apachesolr_static_response_cache(); $query = apachesolr_current_query(); switch ($delta) { case 'project_solr_categories': // Find the parent term for this query. $project_vid = _project_get_vid(); if (isset($response->responseHeader->params->fq)) { foreach ($response->responseHeader->params->fq as $query_snippet) { if (preg_match('/^im_vid_'. _project_get_vid() .':(.*)$/', $query_snippet, $matches)) { $tid = trim($matches[1]); $term = taxonomy_get_term($tid); if ($term->vid == $project_vid) { $project_type = $term; break; } } } } if (!isset($project_type)) { // This facet cannot process generic queries. return; } $facet = 'im_vid_'. _project_get_vid(); $terms = array(); // Get the terms at the current depth. $current_tid = $project_type->tid; foreach ($query->get_filters() as $filter) { if ($filter['#name'] == 'tid') { $current_tid = $filter['#value']; break; } } $current_level_terms = array(); $tree = taxonomy_get_tree(_project_get_vid(), $current_tid, -1, 1); foreach ($tree as $term) { $current_level_terms[$term->tid] = $term; } foreach ($response->facet_counts->facet_fields->$facet as $tid => $count) { $active = $query->has_filter('tid', $tid); if (!isset($current_level_terms[$tid]) && (!$active || $tid != $current_tid)) { continue; } $unclick_link = ''; $term = taxonomy_get_term($tid); $new_query = clone $query; $path = $new_query->get_path(); $options = array(); if ($active) { $new_query->remove_filter('tid', $term->tid); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_unclick_link', $term->name, $path, $options); } else { $new_query->add_filter('tid', $term->tid); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_facet_link', $term->name, $path, $options, $count, $active, $response->numFound); } $countsort = $count == 0 ? '' : 1 / $count; // if numdocs == 1 and !active, don't add. if ($response->numFound == 1 && !$active) { // skip } else { $terms[$active ? $countsort . $term->name : 1 + $countsort . $term->name] = $link; } } $vocab = taxonomy_vocabulary_load(_project_get_vid()); if (!empty($terms)) { ksort($terms); // The currently selected term should be first. if (isset($terms[$current_tid])) { $current_term = $terms[$current_tid]; unset($terms[$current_tid]); $terms = array_merge(array($current_tid => $current_term), $terms); } return array( 'subject' => $vocab->name, 'content' => theme('apachesolr_facet_list', $terms, 200), ); } break; case 'project_solr_compability': if (module_exists('project_release')) { $facet = 'im_project_release_api_tids'; $terms = array(); $active_terms = project_release_compatibility_list(); $active_term_counts = array(); if (isset($response->facet_counts->facet_fields->$facet)) { foreach ($response->facet_counts->facet_fields->$facet as $tid => $count) { if (!empty($active_terms[$tid])) { $active_term_counts[$tid] = $count; } } } foreach ($active_terms as $tid => $term_name) { if (!empty($active_term_counts[$tid])) { $active = $query->has_filter('im_project_release_api_tids', $tid); $new_query = clone $query; $path = $new_query->get_path(); $new_query->remove_filter('im_project_release_api_tids', $term->tid); $options = array(); if ($active) { $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_unclick_link', $term_name, $path, $options); } else { $new_query->add_filter('im_project_release_api_tids', $tid); $options['query'] = project_solr_append_api_term($new_query->get_url_queryvalues(), $tid); $link = theme('project_solr_no_count_facet_link', $term_name, $path, $options, $active, $response->response->numFound); } $terms[$term_name] = $link; } } if (!empty($terms)) { return array( 'subject' => t('Filter by compatibility'), 'content' => theme('apachesolr_facet_list', $terms, 200), ); } } break; case 'project_solr_text': return array( 'subject' => t('Search @project_type', array('@project_type' => $query->get_query_basic())), 'content' => drupal_get_form('project_sort_freetext', $query->get_path()), ); } } /** * Build project browsing navigation form. */ function project_solr_browse_projects_form(&$form_state, $project_type, $path) { drupal_add_css(drupal_get_path('module', 'project_solr') .'/project_solr.css'); $response = apachesolr_static_response_cache(); $query = apachesolr_current_query(); $text = $query->get_keys(); // Get the terms at the current depth. $current_tid = ''; foreach ($query->get_filters() as $field) { if ($field['#name'] == 'tid') { $current_tid = $field['#value']; break; } } // Create a drop-down for sub-terms of primary project terms. $tree = taxonomy_get_tree(_project_get_vid(), $project_type->tid, -1, 1); foreach ($tree as $term) { $terms[$term->tid] = $term->name; } if (!empty($terms)) { $vocab = taxonomy_vocabulary_load(_project_get_vid()); asort($terms); $terms = array('' => t('- Any -')) + $terms; $form['tid'] = array( '#type' => 'select', '#options' => $terms, '#title' => t('@type categories', array('@type' => $project_type->name)), '#default_value' => $current_tid, ); } // Add the version selection field to our form. $form['api_version'] = project_solr_get_api_version_field($query); // Retrieve all the vocabularies related to project so that the facets can be built. $vocabs = project_get_related_tids_map(TRUE); foreach ($vocabs as $vid => $vocab) { if ($project_type->tid == variable_get('project_type_associated_tid_' . $vid, NULL)) { $selected = array(); // Extract selected values from our filters. foreach ($query->get_filters() as $filter) { if ($filter['#name'] == 'im_vid_'. $vid) { $selected[] = $filter['#value']; } } // Build checkbox items for all terms in related vocabularies. $terms = array(); $project_types = project_get_project_types(); foreach ($project_types as $term) { $terms[$term->tid] = $term->name; } asort($terms); $form['im_vid_' . $vid] = array( '#title' => t($vocab->name), '#type' => 'checkboxes', '#options' => $terms, '#default_value' => $selected, ); } } // Add the project sandbox fields. Solr allows use of 0 and 1 to match false and true. $form['bs_project_sandbox'] = array( '#title' => t('Status'), '#type' => 'select', '#options' => array( '0' => t('Full projects'), '[* TO *]' => t('All projects'), '1' => t('Only sandbox projects'), ), '#default_value' => '0', ); // Extract the project sandbox value from our filters. if ($filters = $query->get_filters('bs_project_sandbox')) { // There can be only one. $filter = end($filters); $form['bs_project_sandbox']['#default_value'] = $filter['#value']; } $form['bs_project_sandbox']['#access'] = variable_get('project_solr_show_sandbox_ui', FALSE); $form['text'] = array( '#title' => t('Search @project_type', array('@project_type' => $project_type->name)), '#type' => 'textfield', '#default_value' => $text, '#size' => 20, ); // Add the sort selection field to our form. $form['solrsort'] = project_solr_get_solrsort_field(); $form['path'] = array( '#type' => 'value', '#value' => $path, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Search'), ); return $form; } /** * Return a Form API array for a sort selector field. * * @param string $label * Optional form label for the sort selector field. * * @return * Form array for the sort selector field. */ function project_solr_get_solrsort_field($label = NULL) { // Pull the current query, so that we can default the sorts // and provide an accurate list of available sorting methods. $query = apachesolr_current_query(); // Pull the currently selected sort. $solrsort = $query->get_solrsort(); // Pull all of the available sorts and create an options array. $options = array(); $available_sorts = $query->get_available_sorts(); foreach ($available_sorts as $name => $field) { $options[$name . ' ' . $field['default']] = $field['title']; } if (!isset($label)) { $label = t('Sort by'); } return array( '#title' => $label, '#type' => 'select', '#default_value' => $solrsort['#name'] . ' ' . $solrsort['#direction'], '#options' => $options, ); } function project_solr_browse_projects_form_submit($form, &$form_state) { include_once drupal_get_path('module', 'project_solr') .'/ProjectSolrQuery.php'; $query = new ProjectSolrQuery(apachesolr_get_solr(), $form_state['values']['text'], '', '', $form_state['values']['path']); if (!empty($form_state['values']['tid'])) { $query->add_filter('tid', $form_state['values']['tid']); } if (!empty($form_state['values']['api_version'])) { $query->add_filter(variable_get('project_solr_project_release_api_tids_alias', 'api_version'), $form_state['values']['api_version']); } // Loop over all project-related vocabularies and create filters // for any values that have been posted. $vocabs = project_get_related_tids_map(TRUE); foreach ($vocabs as $vid => $vocab) { $values = array_filter($form_state['values']['im_vid_' . $vid]); if (!empty($values)) { foreach($values as $value) { $query->add_filter('im_vid_'. $vid, $value); } } } // Rewrite solrsort parameter with API version tid, if necessary. $solrsort = explode(' ', $form_state['values']['solrsort']); if (module_exists('project_release') && !empty($form_state['values']['api_version'])) { if ($solrsort[0] == 'ds_project_latest_release' || $solrsort[0] == 'ds_project_latest_activity') { $solrsort[0] .= '_'. $form_state['values']['api_version']; } } if ($solrsort[0] == '' && empty($form_state['values']['text'])) { drupal_set_message(t('No search text submitted for a relevancy sort. The default sorting method was used instead.')); } // Add the project sandbox filter. $query->add_filter('bs_project_sandbox', $form_state['values']['bs_project_sandbox']); // Add all project-specific sorts so that the sort set in set_solrsort() will // work as intended. project_solr_add_sorts($query, variable_get('project_solr_project_release_api_tids_alias', 'api_version')); $query->set_solrsort($solrsort[0], $solrsort[1]); $query_values = $query->get_url_queryvalues(); if (!empty($form_state['values']['text'])) { $query_values['text'] = $query->get_query_basic(); } // By setting this as an array, FAPI is going to hand it to drupal_goto(). // drupal_goto() itself doesn't care about the keys, but we define them here // to make the code more self-documenting, especially if people are trying // to alter this form (e.g. in drupalorg_search.module). $form_state['redirect'] = array( 'path' => $query->get_path(), 'query' => $query_values, ); } /** * Append the API tid to selected fields that might be in the string. */ function project_solr_append_api_term($values, $tid) { $api_fields = array('ds_project_latest_release', 'sis_project_release_usage', 'ds_project_latest_activity'); foreach($values as $k => $v) { if (in_array($k, $api_fields)) { unset($values[$k]); $values[$k . '_' . $tid] = $v; } } return $values; } /** * Form callback; display a free text form. */ function project_sort_freetext(&$form_state, $base_path) { $form = array(); $form['text'] = array( '#type' => 'textfield', '#default_value' => $_GET['text'], '#size' => 20, ); $form['path'] = array( '#type' => 'value', '#value' => $base_path, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); return $form; } /** * Submit handler for project_sort_freetext(). */ function project_sort_freetext_submit($form, &$form_state) { if ($query = apachesolr_current_query()) { $queryvalues = $query->get_url_queryvalues(); } else { $queryvalues = array(); } $queryvalues['text'] = $form_state['values']['text']; unset($queryvalues['solrsort']); $form_state['redirect'] = array($form_state['values']['path'], $queryvalues); } //---------------------------------------- // Public helper methods //---------------------------------------- /** * Build and execute a Solr query to find project nodes. * * @param string $base_path * The base path to use for the Solr query. * @param array $filters * Optional array of Solr filters to add to the query. * * @return * An ApacheSolr $query object. */ function project_solr_run_project_query($base_path, $filters = array()) { $filterstring = isset($_GET['filters']) ? $_GET['filters'] : ''; $query = apachesolr_drupal_query('', $filterstring, '', $base_path); // Figure out all the fields we need to use as facets. // First handle all the implicit filters we're going to add ourselves. $facet_fields = array( 'type', 'im_project_release_api_tids', ); // Now add any fields used for optional filters from our caller. if (!empty($filters)) { foreach ($filters as $filter) { $facet_fields[] = $filter['key']; } } $params = array( // The fields to return. 'fl' => 'id,nid,title', 'start' => 0, 'rows' => 5, // TODO: make this configurable. 'facet.field' => $facet_fields, // Filters are set below via explicit calls to add_filter() rather than // here in the $params array. This is because any filters passed to // apachesolr_drupal_query() will override filters set via the $params // array during the prepare_query() invocation. 'fq' => array(), 'facet' => 'true', 'facet.mincount' => 1, 'facet.sort' => 'true', 'sort' => variable_get('project_solr_default_sort', 'sort_title asc'), ); apachesolr_search_add_facet_params($params, $query); // Allow modules to alter the query prior to statically caching it. // This can e.g. be used to add available sorts. foreach (module_implements('apachesolr_prepare_query') as $module) { $function_name = $module . '_apachesolr_prepare_query'; $function_name($query, $params, 'project_solr_run_project_query'); } // We add our fields after the prepare_query() because prepare_query() // generates a call to parse_filters() which destroys anything that // does not get passed into the query on construction. // We explicitly filter the blocks to only show projects with the right // category. These values are not passed in the url, so we add them here // ourselves to ensure we only get the content we want. $query->add_filter('type', 'project_project'); if (!empty($filters)) { foreach ($filters as $filter) { $query->add_filter($filter['key'], $filter['value']); } } // Cache the built query. Since all the built queries go through // this process, all the hook_invocations will happen later. apachesolr_current_query($query); // This hook allows modules to modify the query and params objects. apachesolr_modify_query($query, $params, 'project_solr_run_project_query'); $solr = apachesolr_get_solr(); $response = $solr->search($query->get_query_basic(), $params['start'], $params['rows'], $params); apachesolr_static_response_cache($response); apachesolr_has_searched(TRUE); return $query; } /** * Return a Form API array for an API version selector field. * * @param object $query * An existing query object that we can check for existing filters. * @param string $label * Optional form label for the version selector field. * * @return * Form array for the API version selector field. */ function project_solr_get_api_version_field($query, $label = NULL) { if (module_exists('project_release')) { $current_tid = ''; $terms = array(); $active_terms = project_release_compatibility_list(); foreach ($active_terms as $tid => $term_name) { $active = $query->has_filter('im_project_release_api_tids', $tid); if ($active) { $current_tid = $tid; } $terms[$tid] = $term_name; } if (!empty($terms)) { $terms = array('' => t('- Any -')) + $terms; if (!isset($label)) { $label = t('Filter by compatibility'); } return array( '#title' => $label, '#type' => 'select', '#options' => $terms, '#default_value' => $current_tid, ); } } } /** * Adds project-specific sorts to the query object * * Adds project-specific sorts to the query object to allow all queries to * leverage project-specific sorting if relevant. This is necessary to allow * site-wide search to sort by any project-specific sorting method. This is * called by hook_prepare_query() to add the sorts into all queries, but * separated out so that a hook_prepare_query() invocation is not necessary * if only the project sorts are desired. We filter the list of sorts to only * provide meaningful options for Latest release and Recent activity so that * we don't end up with multiple sorts with the same name. This is particularly * relevant for how the sorts display in the apachesolr generated sort block. * * @param object &$query * An existing query object to add sorts into. * @param string $api_filter * A string representing the name of the filter where our version tids will * be located. This is necessary since, at times, both the base string name * im_project_release_api_tids and the alias are passed in. * * @see apachesolr_block */ function project_solr_add_sorts(&$query, $api_filter = 'im_project_release_api_tids') { if (module_exists('project_release')) { // Pull any existing filter on version. $versions = $query->get_filters($api_filter); $tids = array(); foreach ($versions as $version) { $tids[] = $version['#value']; } // If we have no version selected, only present the base sorts. if (empty($tids)) { $query->set_available_sort('ds_project_latest_release', array('title' => t('Last release'), 'default' => 'desc')); $query->set_available_sort('ds_project_latest_activity', array('title' => t('Last build'), 'default' => 'desc')); } else { // If we have versions selected, only present sorts for our selected // versions. Traditionally, this will only be one version, but this // allows for multiple potential versions to be selected. $active_terms = project_release_compatibility_list(); foreach ($tids as $tid) { if (isset($active_terms[$tid])) { $query->set_available_sort('ds_project_latest_release_' . $tid, array('title' => t('Last release'), 'default' => 'desc')); $query->set_available_sort('ds_project_latest_activity_' . $tid, array('title' => t('Last build'), 'default' => 'desc')); } } } } if (module_exists('project_usage')) { $query->set_available_sort('sis_project_release_usage', array('title' => t('Most installed'), 'default' => 'desc')); } } //---------------------------------------- // Theme-related functions //---------------------------------------- /** * Perform the business logic to render search results for project-related searches. */ function project_solr_render_search_result($result) { $project = node_load($result->nid); $project = node_build_content($project, TRUE, FALSE); $project->body = $project->teaser; $project->solr_result = $result; if (!empty($project->project_release['releases'])) { $project->download_table = project_release_table($project, 'recommended', 'all', t('Version'), FALSE, FALSE); } $project->links = array(); $project->links['read_more'] = array( 'title' => t('Find out more'), 'href' => "node/$project->nid", ); if (!empty($project->project_issue['issues'])) { $project->links['issues'] = array( 'title' => t('Bugs and feature requests'), 'href' => 'project/issues/'. $project->project['uri'], ); } return theme('project_summary', $project); } function theme_project_solr_no_count_facet_link($facet_text, $path, $options = array(), $active = FALSE, $num_found = NULL) { $options['attributes']['class'][] = 'apachesolr-facet'; if ($active) { $options['attributes']['class'][] = 'active'; } $options['attributes']['class'] = implode(' ', $options['attributes']['class']); return apachesolr_l($facet_text, $path, $options); } //---------------------------------------- // Version-selector form //---------------------------------------- /** * This generates a form containing version selection and a submit button. * * Generate a form with a version selection to allow filtering page content * based on the API compatibility version. Also includes path to allow the * form to potentially submit to other urls if desired. * * @param string $path * The base path to which the version form will redirect. * @param string $label * An optional label for the form element. */ function project_solr_version_form(&$form_state, $path, $label = NULL) { $query = apachesolr_current_query(); $form = array( '#attributes' => array('class' => 'clear-block'), ); // Add version select field to our form. $version_alias = variable_get('project_solr_project_release_api_tids_alias', 'api_version'); $form[$version_alias] = project_solr_get_api_version_field($query, $label); $form['path'] = array( '#type' => 'value', '#value' => $path, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Search'), ); return $form; } /** * Create a query with the right version filter and redirect to the right page. * * Create a new query, add any version filtering if it was selected in the * form, and redirect back to the relevant page with the appropriate filter * string. */ function project_solr_version_form_submit($form, &$form_state) { // We create a new query with our base path so that we don't need to remove // any existing drupal_core selection, and so that the implict type and // module tid filters don't end up in the url string. $query = apachesolr_drupal_query('', '', '', $form_state['values']['path']); $version_alias = variable_get('project_solr_project_release_api_tids_alias', 'api_version'); if (!empty($form_state['values'][$version_alias])) { $query->add_filter($version_alias, $form_state['values'][$version_alias]); } $form_state['redirect'] = array($query->get_path(), $query->get_url_queryvalues()); } //---------------------------------------- // Category page and related functions //---------------------------------------- /** * Page callback for the listing of per-type categories. * * @param $project_type * Fully-loaded taxonomy term object for the project type. * * @return * Rendered page output for the project/%project_type/categories pages. */ function project_solr_category_page($project_type) { $tree = taxonomy_get_tree(_project_get_vid(), $project_type->tid); if (empty($tree)) { return drupal_not_found(); } drupal_set_title(t('@project_type categories', array('@project_type' => $project_type->name))); $categories = array(); foreach ($tree as $category_term) { $items = project_solr_fetch_category_items($project_type, $category_term); if (!empty($items)) { $categories[$category_term->tid] = array( 'title' => check_plain($category_term->name), 'items' => $items, ); } } $search_path = 'project/' . drupal_strtolower($project_type->name) . '/categories'; $version_form = drupal_get_form('project_solr_version_form', $search_path); return theme('project_solr_category_page', $project_type, $categories, $version_form); } /** * Render the markup for the per-project type categories landing pages. * * @param $project_type * Fully-loaded taxonomy term object for the project type. * @param $categories * Nested array of information about categories and projects. The keys are * the taxonomy term IDs (tids) of each category for the given project_type * that has projects associtated with it. The values are arrays with the * keys 'title' for the human-readable (and sanitized) titles of each * category, and 'items', which is an array of project links from the * projects in the category. * @param $version_form * Optional rendered HTML of a version selector form to restrict results to * a given API compatibility term. */ function theme_project_solr_category_page($project_type, $categories, $version_form = '') { $output = ''; if (!empty($version_form)) { $output .= $version_form; } // TODO: This could probably use more slickness, a grid with multiple // columns, etc. http://drupal.org/node/888144 foreach ($categories as $tid => $category) { $output .= '

' . $category['title'] . '

'; $output .= theme('project_solr_category_list', $category['items']); } return $output; } /** * Gather the items for a given project category. * * @param $project_type * The fully-loaded taxonomy term for the project type. * @param $category_term * The fully-loaded taxonomy term for the category. * * @return * Array of projects from the given category. */ function project_solr_fetch_category_items($project_type, $category_term) { $filters = array(); $filters[] = array( 'key' => 'tid', 'value' => $category_term->tid, ); $base_path = 'project/' . drupal_strtolower($project_type->name); $query = project_solr_run_project_query($base_path, $filters); $query_values = $query->get_url_queryvalues(); $response = apachesolr_static_response_cache(); $items = array(); if ($response->response->numFound > 0) { foreach ($response->response->docs as $doc) { $items[] = l($doc->title, 'node/' . $doc->nid); } } if ($items) { // Add the "more" link. $items[] = array( 'data' => l(t('More @category', array('@category' => $category_term->name)), $query->get_path(), array('query' => $query_values)), 'class' => 'more', ); } return $items; } /** * Render the final markup for a list of projects from a given category. * * @param array $items * Array of links to projects in a given category. * * @return * Formatted HTML markup for the list of project in a category. * * @see theme_project_solr_category_page() * @see theme_item_list() */ function theme_project_solr_category_list($items = array()) { $output = ''; if (!empty($items)) { $output .= theme('item_list', $items); } return $output; }