Take the 2-minute tour ×
Drupal Answers is a question and answer site for Drupal developers and administrators. It's 100% free, no registration required.

I am working on a new Drupal 7 site that is making heavy use of Panels to build pages.

I have a page where a view will be added to a content pane. For various reasons, the view cannot be its own page. This particular view has a Contextual Filter on it, Content: Has taxonomy term ID. View works great with numeric arguments.

I would like my panel page to use the term name as the argument for the view instead of the term ID. And, for various reason, I cannot use the taxonomy/term/%taxonomy_term override that comes with Panels.

If I set up the context option for the page (Settings / Arguments) to use Term Name, I can get the argument to work. My problem is that the vocabulary I need to use has terms with lots of capitalization and spaces in it. So, the URLs end up unreadable.

We have a custom ctools arguments plugin that can handle pathauto-style term names, where the term is lowercased and spaces are converted to dashes, but it was done for Drupal 6. Before I spend the time to rework it for Drupal 7, I wanted to see if there is a better and/or existing solution.

In summary,

  1. I have a panel page with path foo/!term
  2. Panel page has a view with a taxonomy term ID argument, where the view argument should be based on the URL
  3. I would like to have URLs like foo/apple-beet-carrot instead of foo/Apple%20Beet%20Carrot
  4. I cannot use the system override for taxonomy/term/%taxonomy_term

What is the best way to go about this with Drupal 7, Views 3, ctools, and Panels, short of making a new ctools argument plugin?

share|improve this question
    
Hi. I was wondering if you found a simplier and less code heavy solution than to write a custom Ctools plugin ? The part of the problem I'm interested in is the clean URL one (your 3). –  Countzero Mar 26 '13 at 9:52
    
@Countzero Sorry, I just double checked and I did end up writing a ctools argument plugin. Most of our projects have a lot of custom modules anyway, so this wasn't a problem. –  MPD Mar 26 '13 at 13:03
    
OK thanks. BTW, there is functionnality in Views and Panels now to add or remove hyphens at both ends, so I managed to fulfill my use case this time. Have a nice day. –  Countzero Mar 26 '13 at 17:00
add comment

3 Answers

up vote 2 down vote accepted

There may be better routes out there, but I am going to suggest the one I have in mind right now.

You could create your own custom block - that wraps around the Views block display that you already created. Why? Because that way you have full control of the arguments.

Wrapping a Views block display with your own custom block:

  1. Implement hook_block_info() and hook_block_view().
  2. Inside your hook_block_view() call the View display programatically. The View display should be a block (it looks like you got it already configured).
  3. While instantiating the View display, you can grab the arguments from the URL using the arg() function. Since this is your own code, you can do anything that you want with those arguments. For example, you could load a term using an nid-based argument, get the term's proper name, URL-decode it if needed, and then feed it to your display.
  4. Execute the View display, retrieve the results, and return them as the output of your block.

Then instead of embedding in the panel the block that was provided by your View, you embed the one that you created in your custom module. Again, since you called the View programatically, you had full control of the arguments that were passed to it.

It sounds complex, but it's really easier than you think. Here's a plain example of how to call the View (you could modify it for your needs):

$view = views_get_view('my_foobar_view');
$result = $view->execute_display('block_display_baz');
$term_id = arg(2); // Get the term from my-panel/foo/<term id>
$view->set_arguments(array($term_id));
$view->execute();
if(!empty($view->result)) {
  foreach($view->result as $result) {
    // Do something with $result...
  }
}

This G.D.O. document can provide you with more examples on how to programatically embed Views.

share|improve this answer
    
Thanks. This did cross my mind. The biggest downside is that each time I want to make a new page like this, I need to make a new block. Not really a problem complexity-wise, but it does mean that less technical people can't help out with tasks like this. One of my goals is to get more people in my organization working with the UI, so developers can concentrate on complex tasks. –  MPD Dec 13 '11 at 19:21
1  
The other options that I can think of is 1) Intercept the existing hook_menu() key for that current panel page, and modify it in a way that you can control the arguments, i.e., by redirecting that callback to your custom function and after playing with the arguments returning back control to the hook_menu() key that you intercepted, OR 2) Create your own hook_menu() key, based on the existing hook_menu() key for that panel, as opposed to intercepting the existing one, for the same purpose as suggestion #1. You would then do this only once, code-wise should not be very complex either. –  amateur barista Dec 13 '11 at 20:41
    
If you have a working example, post it as another answer and I will likely accept it. I have never unraveled how to do this. –  MPD Dec 13 '11 at 21:12
    
No problem, will do ;) –  amateur barista Dec 13 '11 at 21:21
    
I've done some experimentation with this, and if you don't want to go down the road of making your own ctools argument plugin, this really is the best option. If I have time later today, I will post an alternate (though potentially less desirable) solution. –  MPD Dec 20 '11 at 14:47
add comment

The hook_menu() keys for those panels are arbitrary (defined by you), so I can't post a 100% exact example, but here's one general solution of how to "hack" or intercept an existing hook_menu() item:

  1. Go to the menu_router table.
  2. Search for the URL path that you defined for your panel. You can start by using an exact-match search, and if nothing appears, do a wildcard (%) search such as this: select * from menu_router where path like "%bar%" instead of `...path = "foo/bar/baz". This will tell you everything about that menu item, what module defined it, what function it calls, what are it's arguments, and what loading function for the arguments (if any) there are.
  3. Armed with this information, you can go to the module that defined the menu item. Go to the hook_menu implementation of that module and look for that key. Take a note of the different keys that compose the menu item. Sometimes it is overkill to define your own menu item when all you need is to change is, let's say, the title callback. For this we then use the hook_menu_alter().
  4. Implement hook_menu_alter in your custom module.

Example for item # 4.

/**
 * Implementation of hook_menu_alter.
 *
 * Intercept 'foo/bar/baz' and 'taxonomy/term/%taxonomy_term'.
 */
function mymodule_menu_alter(&$items) {
    // The string to the right is the name of the function that is going to get called
    // to determine that page's title. We just substituted what defined there by
    // someone else with our own function.
    $items['foo/bar/baz']['title callback'] = 'mymodule_panel_title_callback';

    // Here I modify a separate menu item.
    // Redirect current page workflow to my module.
    // Note that the function taxonomy_term_load() will be called with whatever argument is in the
    // third element of the path, so "taxonomy/term/123" will call taxonomy_term_load(123) and the
    // result will be passed to mymodule_taxonomy_term_page().
    $items['taxonomy/term/%taxonomy_term']['page callback'] = 'mymodule_taxonomy_term_page';
    // I'm doing this to state the "obvious" - that taxonomy_term_load() will be passed 123 in "taxonomy/term/123".
    // Also to say that there's not much you can do to to modify this particular menu item attribute.
    // Since %taxonomy_term is part of the menu item key, if you modify that to be something else, such as
    // %mymodule_taxonomy_term, you would be basically defining a new menu item (If I'm not mistaken).
    // If you find yourself in the position that you need to modify the "auto-loader wildcard component", then
    // that might be your cue to define your own menu item, and then call back 'taxonomy/term/%taxonomy_term'
    // from your own callback.
    $items['taxonomy/term/%taxonomy_term']['page arguments'] = array(2);

}

Now to make things more complex, let's say that as part of your problem, you do need to modify the "auto-loader wildcard component" for 'taxonomy/term/%taxonomy_term', and that this menu item is defined by Drupal core taxonomy. Like I stated in my comments above, toying with that on your hook_menu_alter() could possible mess up your menu definitions.

So we are left with two options. Define your own menu item and 1) Call back the menu item defined by Core from your own menu item or 2) Just copy the code that is in the body of the function called by the original menu item to your custom function. If doing #2 and you get an error make your you include the file where the core function rests my using module_load_include(). Note that I used core as an example, but the technique could be applied as well to contrib modules.

/**
 * Implementation of hook_menu().
 */
function mymodule_menu() {
  $items = array();
  // Defining an arbitrary path, change it to what makes logic for you.
  // Using a simple wildcard.
  $items['mymodule/term/%']  = array(
    'page callback' => 'mymodule_taxonomy_term_page',
    // Define other arguments, skipping for the sake of brevity.
  );
  // This menu item will call function mymodule_taxonomy_term_load(), and pass
  // the results of that call to mymodule_taxonomy_term_page.
  // Note that the path I define here conflicts with the path above, and that you should use either or, but not both.
  // Else menu router will get confused and will not know which one to call. The same applies to paths created by
  // core or contrib, it is important not to define them twice.
  $items['mymodule/term/%mymodule_taxonomy_term']  = array(
    'page callback' => 'mymodule_taxonomy_term_page',
    // Stating that mymodule_taxonomy_term_load() will be passed the 3rd argument in the path (count is zero-based).
    'page arguments' => array(2),
    // Define other arguments, skipping for the sake of brevity.
  );

  return $items;
}

// Meantime, in another part of your module...
/**
 * Returns a taxonomy term object based.
 */
function mymodule_taxonomy_term_load($term) {
  // Here you could do all the custom logic you want. Depending on what you put here, you might be
  // able to even support numeric term ids and non-numeric term names in the same path. Example:

  if(is_numeric($term)) {
    $term_object = mymodule_taxonomy_term_load($term);
  } else {
    // String detected.
    // Do some cleanup first, and then try to load a taxonomy term with the cleaned string.
    $clean_term_string = somecleanup_function($term);
    $term_object = taxonomy_get_term_by_name($clean_term_string);
  }

  if($term_object) {
    // Profit!
    return $term_object;
  } else {
    // Return something we can reliably validate against in our callback...
    return FALSE;
  }
}

// And then in your hook_menu() callback...

/**
 * Render a page based on term? 
 *
 * @param var $term - A term object if mymodule_taxonomy_term_load() was successfull, else FALSE.
 */
function mymodule_taxonomy_term_page($term) {
  if($term === FALSE) {
    // Degrade gracefully, e.g., show a custom error page...
  } else {
    // If our example `'taxonomy/term/%taxonomy_term'` is defined by Core, and we did not implement hook_menu_alter() for it,
    // call whatever function that callback defines. Note that we accomplished our mission here by loading $term the
    // way we wanted to in the auto-loader function, so we don't really need much processing here, but we could do more if needed.
    return somefunction_defined_bycore($term);

    // Else the other option was to copy and paste somefunction_defined_bycore()'s function body into here and return it.
    // Do some processing...
    return $something;
  }
}
share|improve this answer
    
Thanks, but I guess I wasn't clear about post an example as another answer. I know how to do custom hook_menu and hook_menu_alter. I have never figured out how hook_menu_alter a panel path, mess with the arguments, and then manually invoke the panels callback to render the page. –  MPD Dec 13 '11 at 23:54
    
Right. So what I was saying in my example's comments is that I don't think you should really mess with the arguments in your hook_menu_alter(). –  amateur barista Dec 14 '11 at 15:57
    
When you do need to alter the way the arguments are handled, then you will need to 1) Create your own hook_menu (with your own arguments, argument callback function, etc), and then 2) From your own hook_menu callback, call the panel callback. That's what I was trying to walk you through in the example above. It's a little complex, but it works for sure. Can't honestly think of a simpler way to get around your problem. –  amateur barista Dec 14 '11 at 15:59
add comment

An alternate to the block based solution that I accepted is to create a custom ctools content type. This is rather sparsely documented outside the help in the module, but there is an example module that ships with ctools (normally in the ctools/ctools_plugin_example directory) and there is another sample is can be found here.

Essentially, this is same method that views (and other modules) use to makes themselves available to use as content for panel panes.

The advantage over blocks is that you can set them up to use a ctools context for passing in arguments (which is a weakness of blocks). We have use these with good success on many projects. Using ctools contexts, instead of directly accessing arg, menu_get_object, or some other method, provides a nice abstraction later to isolate the content from how it got integrated into the site.

However, in this particular case, using panel contexts is also the weakness. The proper method would be to create a ctools content plugin to translate the "pretty" term from the URL to a term ID that the content type would use. You can configure the panel content to pass in a URL argument, but the panel content type would need do the parsing/conversion, which isn't a good practice (can be seen to violate SOLID). If you create a context plugin, it would be able to be used directly with using a view as panel content.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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