First make a dynamically filtered view on the fly like the big boys do!
Then, stick it in a panel! There are other ways of doing it, but by using this simple method you can easily:
- List all car dealers whose monthly sales volume falls within a series of different ranges: http://example.com/dealers-that-sell-between/500/1000
- List all jobs with salaries falling within different ranges with http://example.com/jobs-that-pay-between/100/200.
(By the way, we’re working with: Drupal 5.x, Drupal Handbooks (Go Documentation Team!), Views 1 (thanks merlinofchaos!), Panels 2 (thanks Earl Miles), my favorite beverage.)
Contents of this torrid yet hopefully useful polemic:
It’s all in the docs
Where to start: let’s exhaust off-the-shelf flexibility first
Using arguments with views
Arguments that act like filters
Programming a very simple view from scratch with on-the-fly filters
Using Panels 2 to handle the dynamically filtered view through its URL parameters
Re-use, re-use and re-use again (never start from scratch)
It’s all in the docs
Well, like everything else, it’s all documented in drupal.org: the document team do a terrific job documenting, and you can find just about anything there.
Here I just feel like writing it up clear like with a simple example so I don’t forget what actually worked for me when I needed it.
Relevant references:
- Creating views programmatically: http://drupal.org/node/138828
- Views 1.x module developer API: http://drupal.org/handbook/modules/views/api
- Panels 2.0 handbook docs: http://drupal.org/node/201914
- Views 1 handbook documentation: http://drupal.org/node/109604
Where to start: let’s exhaust off-the-shelf flexibility first
To explain what we are looking for here, let’s use the car dealer content type I was using in the recent article on how to leverage Drupal content types, by way of example.
If I want to simply list car dealers, I can whip up a view that does just that. Leveraging views even more, I can use exposed filters, and develop for myself a regular little interactive dealer browser in just a few minutes, without any code at all (this is described in detail here: Creating an interactive browser. Stick it on the menu.).
Using arguments with views
If I want to add on-the-fly flexibility, I can go even further without coding. I can make use of the views arguments functionality, so that different results will be listed according to how the view is invoked via the URL.
For example, let’s suppose that we want a context sensitive list, without the exposed filters, to show car dealers from a particular state?
To do this, I clone the view (never touch anything that is actually working), and edit it, making some changes to the name and the page url (name: "browse_dealers" => "browse_dealers_by_city", url: "view/browse/dealers-by-city"). I then open the section "Arguments", select the field "Text: City (field_dealer_city), and click on the "Add Argument" button. We click the Save button at the foot of the form in order to make our changes permanent.
(For more detailed help with using Views arguments, see Reference 4 above).
Now, to test our new argument driven View manually, we can simply visit the corresponding url, and we will obtain a list of car dealers from the city. For example, http://example.com/view/browse/dealers-by-city/chicago will list car dealers from Chicago you have entered into the system.
The plot thickens, however, regarding anything not involving exact matches (unless you opt for the use of either wildcards, or the optional PHP argument handling code mentioned below). Let’s suppose we add an integer field indicating sales volume (number of vehicles sold per year). And let’s suppose that we want to publish a page with links to lists of dealers falling within certain sales volume ranges? For example, dealers who sell one to five hundred vehicles per month, dealers who sell five hundred to one thousand vehicles per month, dealers who sell more than a thousand vehicles per month, for example?
When we add the field as an argument, however, there is no straightforward way to indicate ranges. As we know, with filters, we could put two in, one greater than or equal, and the other less than, to get something like this:
- Click to see lists of dealers by sales volume:
- Dealers who sell more than a thousand vehicles per month
- Dealers who sell between five hundred and a thousand vehicles per month
- Dealers who sell less than five hundred vehicles per month
I could create a filtered view of this in just a few minutes, with two filters for each case.
What I definitely need are dynamic on-the-fly filters, so I can call a URL with dynamic arguments that have all the operating power of filters.
Of course, something we ccould do is to use the built-in option to add code via the PHP code Argument Handler option found in the arguments section of the View editing form. This is covered in the following excellent article:
Adding Sort via Argument Handler & Visualizing $View Object – http://groups.drupal.org/node/4203
Readers of this article are encouraged to follow up that option. Here, however we are examining another option altogether.
What we really need are…
Arguments that act like filters
That is, filters I can stick on the URL as if they were arguments and have them just work. What we need is to whip up a "few lines of code" to filter views dynamically. But how do we get started with a programmed, dynamically filtered view, with
the operational power of filters and the dynamics of arguments?
We actually have two problems:
1. How to tell Drupal how to handle the following in terms of the menu system:
- http://example.com/view/browse/dealers-by-volume/0/500
- or
- http://example.com/view/browse/dealers-by-volume/500/1000
2. How to actually create the view from scratch on the fly in accordance with the parameters?
Let’s deal with the second one first, then install the solution on the basis of answering the first question (hint: Panels 2).
Programming a very simple view from scratch with on-the-fly filters
So, we need to program and test a simple programmatically generated view. As our environment (we need a built-in environment: never start from scratch) we can either use a php-enabled page, or else the php execute block that comes with Moshe Weitzman’s great devel module (On a site being developed, I always go to Administer > Site building > Blocks and enable the Execute PHP block in the Footer region, for the admin/programmer role only, giving me a nice testbed workspace).
My eyes are on two sources in order to do this, Reference 1 above, and the views.module code itself, admirably commented. And I have a few car dealers in the system with sales volume specified.
We are going to create a view which never existed before in pure PHP code. The Drupal Handbook page seems to be telling us that we need to follow these simple steps:
- Create the view object
- Create its page attribute
- Create some filters
- Do some housekeeping
- Execute the view
- Show the results
The commented result looks something like this:
=', '500', '');
views_load_cache();
views_sanitize_view($view);
print views_build_view('embed', $view, array(), false);
?>
It works! We are rewarded with the following output:
Internacional De Vehiculos | 525 |
Automotores San Jorge S.A | 600 |
Dist.Los Coches La Sabana S.A. | 700 |
Autoniza Ltda | 1250 |
Centrodiesel | 1500 |
Try changing the parameters, and the views type (list, teaser, full). Particularly, use good old print_r to find out the views_build_view()’s field names we will be needing later:
=', '500', '');
views_load_cache();
views_sanitize_view($view);
print_r(views_build_view('items', $view, array(), false));
?>
Now, how did I know what details to fill in for the filters? By "cheating", of course: I made a view and exported it, then I knew what to do (Drupal as a CMS and framework prototyper: Never start a project from scratch!). So I went into Administer > Site building > Views, made a quick view with two exposed filters which would do interactively what I here wish to do programmatically, and then clicked on the "Export" link corresponding to the newly created view. I copied the result into a text editor. I essentially had a PHP standard object instantiation of the view and its parts; checking out the filters part, I got all the info I needed:
$view = new stdClass();
$view->name = 'list_dealers_by_volume';
$view->description = 'Test filter';
$view->access = array (
);
$view->view_args_php = '';
$view->page = TRUE;
$view->page_title = '';
$view->page_header = '';
$view->page_header_format = '1';
$view->page_footer = '';
$view->page_footer_format = '1';
$view->page_empty = '';
$view->page_empty_format = '1';
$view->page_type = 'table';
$view->url = 'view/list/dealers-by-volume';
$view->use_pager = TRUE;
$view->nodes_per_page = '10';
$view->sort = array (
);
$view->argument = array (
);
$view->field = array (
array (
'tablename' => 'node',
'field' => 'title',
'label' => '',
'handler' => 'views_handler_field_nodelink',
'options' => 'link',
),
array (
'tablename' => 'node_data_field_dealer_monthly_sales_volu',
'field' => 'field_dealer_monthly_sales_volu_value',
'label' => '',
'handler' => 'content_views_field_handler_group',
'options' => 'default',
),
);
$view->filter = array (
array (
'tablename' => 'node',
'field' => 'type',
'operator' => 'OR',
'options' => '',
'value' => array (
0 => 'dealer',
),
),
array (
'tablename' => 'node_data_field_dealer_monthly_sales_volu',
'field' => 'field_dealer_monthly_sales_volu_value_default',
'operator' => '>',
'options' => '',
'value' => '500',
),
);
$view->exposed_filter = array (
);
$view->requires = array(node, node_data_field_dealer_monthly_sales_volu);
$views[$view->name] = $view;
Using Panels 2 to handle the dynamically filtered view through its URL parameters
So what I have in mind is a table or a cloud of prepackaged ranges to click on, ranges which could occur in several other contexts, with different kinds of ranges (maybe the 626 – 1293 range may take on signifcance for some reason!), and which would fit the parameterized URL, for example:
http://example.com/view/browse/dealers-by-volume/626/1293
would show me all the dealers whose monthly sales volume fell into the range of 626 – 1293 units, for example.
Enter stage left Panels 2.
I simply go into Administer > Site building > Panel pages, click on the add tab, choose any layout, choose any panel name, say dealers_by_volumne, and in the Path field put "dealers-by-volume/%".
That means that this panel will always be called when the URL corresponds to dealers-by-volume/{anything} .
And click Next.
Then, in the content part, I stick the following code into a new custom pane.
- Click on the "+" icon.
- Choose "New custom content".
- Appropriate Title
- Choose PHP Code input format
- Put the following code into the Body section and click on Submit:
'; '; ';
=', arg(1), '');
views_view_add_filter($view, 'node_data_field_dealer_monthly_sales_volu', 'field_dealer_monthly_sales_volu_value_default', '<', arg(2), '');
views_load_cache();
views_sanitize_view($view);
// Let's get creative! We can because we re-use stuff and have the time to be creative!
// Instead of "embed" and just printing that out,
// let's use the more interesting "items" view building option
// (check it out in the views.module docs on this function
$the_dealers = views_build_view('items', $view, array(), false);
print '
Dealer
Monthly sales volume in units
$odd = 1;
foreach ($the_dealers['items'] as $dealer) {
$class = ($odd == 1)?'odd':'even';
print '
' . $dealer -> node_title . '' .
'' . $dealer -> node_data_field_vehiculo_precio_desde_field_vehiculo_precio_desde_value
. '' . $dealer -> node_data_field_dealer_monthly_sales_volu_field_dealer_monthly_sales_volu_value
. '
$odd = ($odd == 1)?0:1;
}
print '
?>
(you can see it makes use of the good old arg() function to work, although a more complex example would actually use a Panels 2 context)
That’s it. Try it, you’ll see:
URL: http://example.com/dealers-by-volume/500/1000
Result:
Dealers by monthly sales volume
Dealer | Monthly sales volume in units | |
---|---|---|
Internacional De Vehiculos | 525 | |
Automotores San Jorge S.A | 600 | |
Dist.Los Coches La Sabana S.A. | 700 |
URL: http://example.com/dealers-by-volume/0/500
Result:
Dealers by monthly sales volume
Dealer | Monthly sales volume in units | |
---|---|---|
Automotores La Floresta S.A. | 50 | |
Continautos. S.A. | 100 |
With re-use, standing on the shoulders of giants, it’s easy: only losers start from scratch.
Re-use, re-use and re-use again (never start from scratch)
Cannot avoid adding in my two-cents regarding the "real men don’t use CMS" fiasco originated by blogger Scott Miller last week in his Why WordPress, Drupal, and other CMS’s are bad for Innovation embarrassment. The writer of this piece has shown absolute ignorance in terms of what it takes for any non-trivial software development project, especially web applications, to survive at all. Just as Picasso spent a large part of his childhood copying the masters, internalized in his consciousness forever and impossible to separate from his work of a lifetime, in art and in science, and certainly in any kind of software development, true greatness comes from bricolage: re-use, that is (There is even a CMS named after this term)!
Starting from scratch, especially using the frameworks of fashion, is a recipe for disaster, and this article is also meant to show by way of example how re-use lays at the heart of true creativity.
Not to mention, of course, that writing code implies testing for bugs and testing for security, and the more you reuse, the more testing (and quality) you have built right into your work.