3

I am porting an existing custom form that takes optional arguments from the URL to preload some fields. I can not use the ?q= method because I must maintain backwards compatibility. For example, both of these URLs are valid:

shop/barn
shop/barn/small/white/red/double/tin

The problem I am running into is how to setup this with Page Manager to accept an optional argument. If I set up my url to be:

shop/barn/%settings

and map %settings to a string, I can get it load the optional arguments! But ... shop/barn does not work. The only way I've figured out to resolve this so far is to create two pages with the exact same panel setup. I would really like to condense this down to one.

I have a feeling the answer lies in something to do with making a custom ctools arguments plugin as the author posted for Panel Pages and Taxonomy Term Arguments. I have tried modifying line 48 in the simplecontext_arg.inc in the ctools_plugin_example module with the following to accomplish this:

if (empty($arg)) {
  $arg[0] = '123';
  return ctools_context_create('simplecontext', $arg);
}

Unfortunately, this displays a blank panel when I visit ctools_plugin_example unlike visiting ctools_plugin_example\123.

This is being implemented on a Drupal Commerce site, so in a perfect world I would also like my panel pages to override the node/%node path with the optional argument so that I can more easily and consistently change the path of my product displays in the future instead of having to manually maintain a bunch of URLs.

I am also open to suggesting something other than Panel Pages, such as overriding the node--barn.tpl.php files.

2 Answers 2

7

My final solution was to create a Page Panel with the following path:

shop/%product/!optionalArgs

% indicates a required argument, ! indicates an optional argument

In order to grab all of the "arguments" or URL path after the product (ex: barn), go to Settings » Arguments, select Settings for optionalArgs, and make sure Get all arguments after this one is checked. Click Finish.

In order to use this with a Clean URL, the panel path needs to be some derivative of the Clean URL because panels does not have access to it. For more details, see merlinofchaos's explanation in the following Panels Issue: Panels, Pathauto and Arguments

Let's say I have a url. It can be any URL, but for this example it's "node/12".

Let's say I have an alias and it's generated by pathauto, and it's something like video/type/12/this-is-the-title-of-the-video and it's aliased to "node/12"

If I visit http://example.com/video/type/12/this-is-the-title-of-the-video then what Drupal thinks the URL was was, in fact, 'node/12'. Panels never has a chance to do anything with video/type/12/foo because that URL doesn't really exist.

The reason, then, that adding some other piece to the URL makes it work is that aliases don't accept arguments. They must be exact. So the problem, in that case, is the alias itself is actually taking the page away from where you want it to go.

1

I ran into a similar scenario. I've seen a lot of questions, answers, and articles that recommend various methods of passing hierarchical or nested URL patterns into views arguments or contextual filters, but they were all too generic or didn't work for my situation, which I feel is a very common one.

I'm working with Drupal Commerce products and hierarchical taxonomy terms and I wanted URL structures like this:

Taxonomy:
store/parent-category
store/parent-category/child-category
store/parent-category/child-category/grandchild-category

Product Display:
store/parent-category/child-category/grandchild-category/product-title

In PathAuto I defined the following patterns:

Product Display paths:
store/[node:field-product-display-category:parents:join-path]/[node:field-product-display-category:name]/[node:title]

Product Taxonomy paths:
store/[term:parents:join-path]/[term:name]

The field-product-display-category mentioned in the token above is coming from a Term Reference field on my Product Display content type.

product manage fields

So a default taxonomy page would have the path store/parent-category/child-category/grandchild-category. Each one of those nested taxonomy pages needed to show all the products referenced by child terms. So store/parent-category would need to show all products belonging to child-category and grandchild-category.

I also needed the taxonomy names in the URL instead of the taxonomy term IDs. I created a view page with a Content: Has taxonomy term ID (with depth) contextual filter because I might have any number of possible nested taxonomies and I wanted the parent taxonomy to show all results for the child taxonomies.

term with depth

I tried to use the contextual filter validation method of converting the term name in the url into its corresponding term id and replace spaces with dashes, but my category names have special characters like /, &, +, and -, which meant decoding the url didn't always match the original term name.

taxonomy term validator

To get around this I created a custom views contextual filter validator, which could correctly parse the term name out of the url and figure out the corresponding term id by querying the url_alias table. After querying a particular path once, the result is cached for better performance.

To create the custom contextual filter validator I followed this example: http://www.joetsuihk.com/en/en/custom_views_argument_validator_views_plugin_argument_validate

The info file:

name = Example Module
package = "Example"
description = Example Module custom views argument validator
core = 7.x

files[] = plugins/example_module_plugin_argument_validate_url.inc

The module:

/**
 * @file example_module/example_module.module
 * Implement hook_views_api - load up the custom views include files
 */
function example_module_views_api() {
  return array(
    'api' => 3.0,
    'path' => drupal_get_path('module', 'example_module'),
  );
}

The views include file:

/**
 * @file example_module/example_module.views.inc
 * Defines a custom contextual filter validator
 */
function example_module_views_plugins() {
  return array(
    'argument validator' => array(
      'taxonomy_url_alias' => array(
        'title' => t('Taxonomy URL Alias'),
        'handler' => 'example_module_plugin_argument_validate_url',
        'path' => drupal_get_path('module', 'example_module') . '/plugins',
      ),
    ),
  );
}

The contextual filter handler:

/**
 * @file example_module/plugins/example_module_plugin_argument_validate_url.inc
 * Contains the taxonomy url alias contextual filter validation plugin.
 */

/**
 * Validate whether an argument (contextual filter) is a valid
 * @ingroup views_argument_validate_plugins
 */
class example_module_plugin_argument_validate_url extends views_plugin_argument_validate {

  private $intAddUnixTime = 604800;
  private $error = FALSE;

  function validate_argument($argument) {
    $view = &$this->view;
    $handler = &$this->argument;

    if (!empty($view->args)) {
      //reconstruct the url from the views argument array
      $strPath = implode("/", $view->args);

      //get the taxonomy term id based on the url, not the term name
      $objAlias = $this->getTermId($strPath);

      //split the url_alias's source column up into an array.
      //the source column contains the term id at the end of a path like 'taxonomy/term/123'.
      $arrTermData = explode("/", $objAlias->source);
      $tid = end($arrTermData);
      $term = taxonomy_term_load($tid);

      //manually assign the term id to be the view's argument
      $handler->argument = $term->tid;
      return true;
    }
    else {
      return false;
    }
  }

  /**
   * Get the taxonomy term id from either the url_alias table or from persistent cache
   * @param string $strPath like 'parent-category/child-category/grandchild-category'
   * @return object containing the result of the url_alias table or persistent cache lookup
   */
  private function getTermId($strPath) {
    $objReturn = NULL;
    //attempt to get the taxonomy term id from cached url_alias table result object
    $objReturn = $this->getCache($strPath);

    //if cache is not set, do a select query
    if ($objReturn == NULL) {
      try {
        if(!$this->error) {
          $objReturn = $this->_dbSelect($strPath);
          //cache the response
          $this->setCache($strPath, $objReturn);
        }
        else {
          //log some error because $this->error was true
        }
      }
      catch (Exception $ex) {
        $this->error = TRUE;
        //log an exception
      }
    }

    return $objReturn;
  }

  /**
   * Run a query to look up the taxonomy term id based on the url alias path
   * @param string $strPath url alias like 'parent-category/child-category/grandchild-category'
   * @return object results of the query
   */
  private function _dbSelect($strPath) {
    $result = NULL;
    try {
      //The alias stored in the url_alias table is going to be prepended with 'store/' because of my pathauto taxonomy pattern
      $result = db_select("url_alias", "ua")
        ->fields('ua', array('pid','source','alias'))
        ->condition('alias', 'store/'.$strPath, '=')
        ->condition('source', 'taxonomy/term%', 'LIKE')
        ->range(0,1)
        ->execute()
        ->fetchObject();
    }
    catch(Exception $ex) {
      //log an exception
    }

    //$result is an object like:
    //$result->pid //'12345'
    //$result->source //'taxonomy/term/123'
    //$result->alias //'store/parent-category/child-category/grandchild-category'
    //$result->uid //1
    return $result;
  }

   /**
   * Get an object from cache
   * @param  string $strPath used as part of the cache id
   * 'cache_taxonomy_url_alias' is a custom cache bin
   * @return object data from the persistent cache
   */
  private function getCache($strPath) {
    $objCache = cache_get('example_module_plugin_argument_validate_url' . '::' . 'store/'.$strPath, 'cache_taxonomy_url_alias');
    if (isset($objCache->data)) {
      return $objCache->data;
    }
    else {
      return NULL;
    }
  }

  /**
   * Store an object in cache
   * @param  string $strPath used as part of the cache id
   * @param  object $objData contains the url alias table results
   * @return void
   */
  private function setCache($strPath, $objData) {
    cache_set('example_module_plugin_argument_validate_url' . '::' . 'store/'.$strPath, $objData, 'cache_taxonomy_url_alias', time() + $this->intAddUnixTime);
  }
}

After that I selected my custom contextual filter validator:

select custom argument validator

The next issue I ran into was that my views path was the same as my taxonomy pattern. I visited store/parent-category/child-category and expected the view to handle the display, but instead it went to the default taxonomy page. Worst of all, it didn't display any products as belonging to the child-category because the products actually belong to the grandchild-category taxonomy nested underneath it. I was seeing the 'There is currently no content classified with this term' message.

To overcome this, I just gave my view a different path like store-view/%.

This still doesn't satisfy my needs because I really need the view to be displayed with other blocks on a panel. To solve this I created a content pane display type on the view to be embedded in a panel page.

I set up the pane settings to use the panel path:

panel config

And to use the first panel argument as the view's argument data source:

panel arg

Then for the content pane's contextual filter validation, I selected the custom Taxonomy URL Alias validator:

custom validator

Then tell it to link to the other view's Categories Page display that I created earlier, which is also using the Taxonomy URL Alias validator:

pane display

Then I created a panel page with a different path to contain the views content pane I just created:

panel page

I configured the argument to be a String instead of a Taxonomy Term ID

panel string argument

In the argument settings, select Get all arguments after this one because I needed to send the whole path parent-category/child-category/grandchild-category into the view content pane, not just the first argument parent-category:

enter image description here

That's it, now navigating to 'panel-page/parent-category/child-category' looks up the term id for 'child-category' and displays all the products belonging to the 'child-category' and 'grandchild category', since the contextual filter is configured for a term id (with depth).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.