When a user visits a certain page I want to hide a certain facet block and also force it to have a certain value. Hiding the block is easy enough with block visibility settings. But Is it possible to force the facet to be have a certain value (so that dependent facets show up) without resorting to redirecting the user to an ugly url with query string?

In other words, I want the user to be able to go to:

http://www.mywebsite.com/browse/assets

And get facets to display as if they were at:

http://www.mywebsite.com/browse/assets/?f[0]=bundle%3Aasset

I'm aware that I can force the bundle facet by creating a new apache solr search page with custom filter (bundle:asset), and this works to restrict results, but it does not automatically set the value of the facet api block, so dependent facet blocks are not triggered as I want them to be.

CommentFileSizeAuthor
#3 facetapi-1715202-3.jpg66.69 KBcpliakas
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

cpliakas’s picture

Hi JordanMagnuson.

Thanks for the post. My recommendation would actually be to use the search page with a custom filter and then build a dependency plugin to match your condition for three reasons. The first is that it will be tricky to do what you are trying to accomplish with Facet API, and the second is that it will take more processing in that facets are much more expensive than straight filters, and Facet API will also have to re-process the active items. The third reason is that the value is still going to show up in the breadcrumbs, so you are going to have to code your way around that.

With that being said, I am thinking that the easiest way to do what you are asking is to execute the following snippet somewhere before the facets are rendered. Effectively you will alter the facet "parameters" containing the filters to do what you are trying to accomplish.


// The searcher will be named apachesolr@`env-id`.
$adapter = facetapi_adapter_load($searcher);
$url_processor = $adapter->getUrlProcessor();

// Check if the filter is already active.
$filter = "bundle:asset"
$active_items = $adapter->getAllActiveItems();
if (!isset($active_items[$filter])) {

  // If not, get and tamper with your parameters here.
  $filter_key = $url_processor->getFilterKey();
  $params = $url_processor->fetchParams();
  $params[$filter_key][] = $filter;
  $adapter->setParams($params, $filter_key);
}

The code above might be off because I didn't validate it, but play around and see if that works.

Thanks,
Chris

JordanMagnuson’s picture

Thanks much for your reply Chris--I really appreciate it! I'd definitely like to go with whatever solution you think would be best/most performant.

I've got the solr search page with the custom filter, which works to limit results, so the issue then is just how to go about creating a dependency plugin that suits my needs for the given facet block. If you could help point me in the right direction, I'd be extremely grateful.

Let me explain my situation just a little bit further. I've got three solr search pages:

  1. A "regular" solr search page, with a search bar, where the user can search through all content, and filter by facets. This includes a facet block for content type ("asset" or "template"), and two dependent blocks, "type of asset" and "type of template" that only activate if that bundle is selected in the first block (via the "A bundle this field is attached to must be active dependency".)
  2. A "browse assets" page, with no search bar, with custom filter "bundle:asset." Here I want to hide the content type facet block (easily done with block visibility), but I want the "type of asset" block to show (since content types has already been limited to asset by the filter).
  3. A "browse templates" page, set up in the same way as the "browse assets": no search bar, with custom filter "bundle:template." Again, I want to hide the content type facet block, and want the "type of template" block to show.

So the "issue" is with the two dependent facet blocks, "type of template" and "type of asset". These show up on the search page when their dependencies are triggered--which is what I want--but they don't show up on the "Browse" pages. It seems the dependency I need for these blocks is somewhat complex: I want them to show up in two different situations (either/or): show if a bundle this field is attached to is active (on the main search page) OR show if I am on the "Browse Assets" page (or "Browse Templates" page for the "type of template" block).

Have I described the situation clearly enough? Do you think it would be easy enough to write a dependency plugin to get at what I'm after?

cpliakas’s picture

FileSize
66.69 KB

Hm, cool use case. I think this is a great opportunity to show how to build a dependency plugin, so I am going to post some code that you can probably start with and run from there. Just to give some transparency about the dependency plugin system, generally how it works is that the dependencies are satisfied if at least one of the conditions pass. So you can definitely add multiple dependencies to that facet (i.e. content type facet selected or custom filter active) and it will have the either / or effect you are looking for. We already have the content type dependency as you alluded to, so what we are going to do is build a simple dependency plugin to check if a filter is set.

The first thing we are going to do is register our dependency plugin using hook_facetapi_dependencies(), which points to a class containing our dependency plugin.


/**
 * Implements hook_facetapi_dependencies().
 */
function mymodule_facetapi_dependencies() {
  $dependencies = array();

  $dependencies['custom_filter'] = array(
    'handler' => array(
      'label' => t('Custom filter'),
      'class' => 'MymoduleDependencyCustomFilter',
    ),
  );

  return $dependencies;
}

The next thing we are going to do is register the plugin for the facets that we want this dependency to work for by implementing hook_facetapi_facet_info_alter(). This example uses the "bundle" facet, but it could be any facet or combination of facets that you want.


/**
 * Implements hook_facetapi_facet_info_alter().
 */
function mymodule_facetapi_facet_info_alter(array &$facet_info, array $searcher_info) {
  // Only modify node facets related to Apache Solr Search Integration.
  if ('apachesolr' == $searcher_info['adapter'] && isset($searcher_info['types']['node'])) {
    $facet_info['bundle']['dependency plugins'][] = 'custom_filter';
  }
}

Last, we are going to actually write the plugin class. It will be an extension of the abstract class FacetapiDependency, and make sure to put it in Drupal's class registry so it can be autoloaded. See the "files" section. We will override the settingsForm() and getDefaultSettings() methods to define the UI, and we will implement the execute() method to execute the plugin.


/**
 * Dependency plugin that checks whether a custom filter is set.
 */
class MymoduleDependencyCustomFilter extends FacetapiDependency {

  /**
   * Implements FacetapiDependency::execute().
   */
  public function execute() {
    if ($this->settings['custom_filter']) {

      // Get the Apache Solr Search Integration search page we are currently on.
      $pages = &drupal_static('apachesolr_search_page_load', array());
      $page_id = key($pages);

      // Dependency fails if we don't have a search page or custom filter.
      if ($page_id && !empty($pages[$page_id]->settings['fq'])) {

        // Iterate over filters to check if we have a match.
        $filters = $pages[$page_id]->settings['fq'];
        foreach ($filters as $filter) {
          // NOTE: This is only good for really simple use cases!!
          if (FALSE !== strpos($filter, $this->settings['custom_filter'])) {
            return; // This dependency passed, so defer to other dependencies.
          }
        }
      }

      // This dependency failed because there was no match or custom filter.
      return FALSE;
    }
  }

  /**
   * Overrides FacetapiDependency::settingsForm().
   */
  public function settingsForm(&$form, &$form_state) {
    $form[$this->id]['custom_filter'] = array(
      '#title' => t('Custom filter'),
      '#type' => 'textfield',
      '#default_value' => $this->settings['custom_filter'],
      '#description' => t('Checks for the presence of this custom filter.'),
    );
  }

  /**
   * Overrides FacetapiDependency::getDefaultSettings().
   */
  public function getDefaultSettings() {
    return array(
      'custom_filter' => '',
    );
  }
}

If all went well, you will be able to configure your dependency. Simply add the filter you are checking for in the text field, and it will be required for the dependency to pass.

facetapi-1715202-3.jpg

The match in the execute() method will have to be more sophisticated to work for complex use cases, but you get the idea :-).

Good luck,
Chris

JordanMagnuson’s picture

Status: Active » Fixed

Wow, Chris, thanks so much for your help! I have a much better idea of how dependencies work now, and was able to get things working perfectly thanks to your invaluable code examples. I love solr and facet api! Between the two I've managed to replace most of my views-based pages with super-fast solr-based pages.

I did find I needed to modify a couple little bits of the plugin code you provided. Let me know if these corrections look right to you:

The code below just seems to get the id of the first solr page returned, rather than the current page?

      // Get the Apache Solr Search Integration search page we are currently on.
      $pages = &drupal_static('apachesolr_search_page_load', array());
      $page_id = key($pages);

Replaced with:

      // Get the Apache Solr Search Integration search page we are currently on.
      $current_path = current_path();
      $pages = &drupal_static('apachesolr_search_page_load', array());
      foreach ($pages as $page) {
        if ($page->search_path == $current_path) {
          $page_id = $page->page_id;
          break;
        }
      }

The code below seemed to create an AND type of situation where this dependency had to be met in addition to any others, rather than the OR type situation I was looking for:

      // Dependency fails if we don't have a search page or custom filter.
      if ($page_id && !empty($pages[$page_id]->settings['fq'])) {

        // Iterate over filters to check if we have a match.
        $filters = $pages[$page_id]->settings['fq'];
        foreach ($filters as $filter) {
          // NOTE: This is only good for really simple use cases!!
          if (FALSE !== strpos($filter, $this->settings['custom_filter'])) {
            return; // This dependency passed, so defer to other dependencies.
          }
        }
      }

      // This dependency failed because there was no match or custom filter.
      return FALSE;

I just flipped the return logic around, and this seems to work as desired:

      // Dependency fails if we don't have a search page or custom filter.
      if (isset($page_id) && !empty($pages[$page_id]->settings['fq'])) {
        // Iterate over filters to check if we have a match.
        $filters = $pages[$page_id]->settings['fq'];
        foreach ($filters as $filter) {
          // NOTE: This is only good for really simple use cases!!
          if (FALSE !== strpos($filter, $this->settings['custom_filter'])) {
            return TRUE; // This dependency passed, so return TRUE
          }
        }
      }

      // This dependency failed because there was no match or custom filter, so defer to other dependencies.
      return;
cpliakas’s picture

JordanMagnuson, thanks for posting your updates! I think that as long as the code works as expected, then run with it.

The reason why my code checked for the first search page (which I agree is hacky) is because there are some instances where there will be a "%" in the path and it therefore won't match the current_path(). If your pages don't have a %, then your code will work fine. I am wondering if there is some middle ground to solve this use case more reliably, but that is a whole other topic :-)

Anyways, great work, and thanks for posting back.
Chris

cpliakas’s picture

To follow up, you are right, my code is borked. It breaks under certain conditions. As a workaround, I came up with another snippet that I am using in another module. See the apachesolr_stats_get_page_id() function.

JordanMagnuson’s picture

Thanks for the followup, Chris.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

Anonymous’s picture

Issue summary: View changes

Added last paragraph.

JordanMagnuson’s picture

Issue summary: View changes
Status: Closed (fixed) » Active

I've run into another use case for which the above solution does not work.

The situation is this:

I have a site with a bunch of nodes and a boolean field called "patrons_only," which marks items that are only available to members with "patron" status.

I have a default browse page that allows users to browse all nodes on the site via an Apache Solr page, with facet filtering provided via Facet API.

By default, patron_only items are not shown, but I want to allow users to include patron_only items in results via a filter like so:
[ ] Show Patron items

The single checkbox filter can be achieved fairly easily via rewriting, but behind the scenes we have two values with an OR filter:

[X] patron_only:false
[ ] patron_only:true

With the option for the user to select:

[X] patron_only:false
[X] patron_only:true

It's easy to show all of the items, and then allow the user to filter, but the problem is that I want the default display to hide the patron_only items (so I want patron_only:false to be selected). If I set this up via a default solr filter on that page (bm_patron_only:false) then there's no way to later display those items via facet api filter... So once again, I want to force the facet api filter into a default start state for the given page, but find myself unable to do so...

Any thoughts on how best to achieve what I'm after?