Exclude the results of a one-node view from another view (the Featured Feature problem)
Drupal 6 Views 2 has an offset modifier to the "Items to display" property which should make doing this dead simple; just create two displays of the same view with different settings for these two options.
Scroll way down for the resolution in Drupal 5:
Hat tip to:
http://drupal.org/node/131482
dan hak 3:45
featured view shows latest feature only
other one shows everything
all featured are in the everything vocab too
benjamin melançon 3:45
ok, I have the fix
in your second view
add a "Node:ID" argument type to your view. In the Option dropdown, select "Not Equal". Then place the following in the Argument Handling Code:
um, do you mind setting the first view to show exactly one, turning on the devel module, asking it to log queries, and giving me the query the view runs (or the link, and I'll find it)
dan hak 3:47
the first view is already set to show only one
why the other stuff?
benjamin melançon 3:47
yeah, I need the SQL
it'll be a lot faster than figuring it out myself. This will let us exclude the node returned by the first view
<?php
$view = views_get_view('featured_resource');
print '<pre>';
print_r($view);
print '
';
$test = views_build_view('embed', $view);
print_r($test);
print '</pre>';
?>
No raw data available!
The answer must be contained in views_build_view:
<?php
/**
* This builds the basic view.
* @param $type
* 'page' -- Produce output as a page, sent through theme.
* The only real difference between this and block is that
* a page uses drupal_set_title to change the page title.
* 'block' -- Produce output as a block, sent through theme.
* 'embed' -- Use this if you want to embed a view onto another page,
* and don't want any block or page specific things to happen to it.
* 'result' -- return an $info array. The array contains:
* query: The actual query ran.
* countquery: The count query that would be run if limiting was required.
* summary: True if an argument was missing and a summary was generated.
* level: What level the missing argument was at.
* result: Database object you can use db_fetch_object on.
* 'items' -- return info array as above, except instead of result,
* items: An array of objects containing the results of the query.
* 'queries' -- returns an array, summarizing the queries, but does not
* run them.
* @param $view
* The actual view object. Use views_get_view() if you only have the name or
* vid.
* @param $args
* args taken from the URL. Not relevant for many views. Can be null.
* @param $use_pager
* If set, use a pager. Set this to the pager id you want it
* to use if you plan on using multiple pagers on a page. To go with the
* default setting, set to $view->use_pager. Note that the pager element
* id will be decremented in order to have the IDs start at 0.
* @param $limit
* Required if $use_pager is set; if $limit is set and $use_pager is
* not, this will be the maximum number of records returned. This is ignored
* if using a view set to return a random result. To go with the default
* setting set to $view->nodes_per_page or $view->nodes_per_block. If
* $use_pager is set and this field is not, you'll get a SQL error. Don't
* do that!
* @param $page
* $use_pager is false, and $limit is !0, $page tells it what page to start
* on, in case for some reason a particular section of view is needed,
* @param $offset
* If $use_pager == false, skip the first $offset results. Does not work
* with pager.
* without paging on.
* @param $filters
* An array of exposed filter ops and values to use with the exposed filter system
* Array has the form:
* [0] => array('op' => 'foo', 'value' => 'bar'),
* [1] => array('value' => 'zoo'), // for a locked operator, e.g.
* If no array is passed in, views will look in the $_GET array for potential filters
*/
function views_build_view($type, &$view, $args = array(), $use_pager = false, $limit = 0, $page = 0, $offset = 0, $filters = NULL) {
views_load_cache();
// Fix a number of annoying whines when NULL is passed in..
if ($args == NULL) {
$args = array();
}
// if no filter values are passed in, get them from the $_GET array
if ($filters == NULL) {
$filters = views_get_filter_values();
}
views_set_current_view($view);
$view->build_type = $type;
$view->type = ($type == 'block' ? $view->block_type : $view->page_type);
if ($view->view_args_php) {
ob_start();
$result = eval($view->view_args_php);
if (is_array($result)) {
$args = $result;
}
ob_end_clean();
}
$view->use_pager = $use_pager;
$view->pager_limit = $limit;
$view->current_page = $page;
$view->offset = $offset;
// Call a hook that'll let modules modify the view query before it is created
foreach (module_implements('views_pre_query') as $module) {
$function = $module .'_views_pre_query';
$output .= $function($view);
}
$info = _views_get_query($view, $args, $filters);
if ($info['fail']) {
return FALSE;
}
if ($type == 'queries') {
return $info;
}
$query = db_rewrite_sql($info['query'], 'node');
$items = array();
if ($query) {
if ($view->use_pager) {
$cquery = db_rewrite_sql($info['countquery'], 'node', 'nid', $info['rewrite_args']);
$result = pager_query($query, $view->pager_limit, $view->use_pager - 1, $cquery, $info['args']);
$view->total_rows = $GLOBALS['pager_total_items'][$view->use_pager - 1];
}
else {
$result = ($view->pager_limit ? db_query_range($query, $info['args'], $view->current_page * $view->pager_limit + $view->offset, $view->pager_limit) : db_query($query, $info['args']));
}
$view->num_rows = db_num_rows($result);
if ($type == 'result') {
$info['result'] = $result;
return $info;
}
while ($item = db_fetch_object($result)) {
$items[] = $item;
}
}
if ($type == 'items') {
$info['items'] = $items;
return $info;
}
// Call a hook that'll let modules modify the view just before it is displayed.
foreach (module_implements('views_pre_view') as $module) {
$function = $module .'_views_pre_view';
$output .= $function($view, $items);
}
$view->real_url = views_get_url($view, $args);
$output .= views_theme('views_view', $view, $type, $items, $info['level'], $args);
// Call a hook that'll let modules modify the view just after it is displayed.
foreach (module_implements('views_post_view') as $module) {
$function = $module .'_views_post_view';
$output .= $function($view, $items, $output);
}
return $output;
}
?>
Bingo. View type "items" -
<?php
$view = views_get_view('featured_resource');
$test = views_build_view('items', $view);
print '<pre>';
print_r($test);
print '</pre>';
?>
Result of the print of test view using items:
Array
(
[query] => SELECT node.nid, node.created AS node_created_created FROM {node} node LEFT JOIN {term_node} term_node ON node.nid = term_node.nid LEFT JOIN {term_hierarchy} term_hierarchy ON term_node.tid = term_hierarchy.tid WHERE (node.type IN ('resource')) AND (term_node.tid IN ('52')) ORDER BY node_created_created DESC
[countquery] => SELECT count(node.nid) FROM {node} node LEFT JOIN {term_node} term_node ON node.nid = term_node.nid LEFT JOIN {term_hierarchy} term_hierarchy ON term_node.tid = term_hierarchy.tid WHERE (node.type IN ('resource')) AND (term_node.tid IN ('52'))
[items] => Array
(
[0] => stdClass Object
(
[nid] => 464
[node_created_created] => 1203454387
)
)
)
So, $test[items][0]->nid;
is the value I'm looking for.
Resolution
Putting it all together in Drupal 5 Views 1, no PHP is necessary in Drupal 6 Views 2:
add a "Node:ID" argument type to your view. In the Option dropdown, select "Not Equal". Then place the following in the Argument Handling Code (minus the
<?php
?>
tags used here for formatting):
<?php
$view = views_get_view('featured_resource');
$exclude = views_build_view('items', $view);
$args[0] = $exclude[items][0]->nid;
?>
Comments
Code isn't right for views2
Your code isn't right for views2. Here's example of correct one:
$exclude = views_get_view('sticky');
$exclude->args = $view->args;
$exclude->execute();
foreach ($exclude->result as $node) {
$nids[] = $node->nid;
}
return implode(',', $nids);
(And enable "Allow multiple terms per argument" in yur view)
ok but what to do when the result of the first view is random
Cause you re-execute the first view before building the second, but the result may be different in case of a random result.
So what to do in that case ?
Thanks for helping me or give me some ideas.
Eric
Views 2 code
Benjamin and Neochief thanks for your help on this.
@neochief if the 'exclude' view returns an empty result set for whatever reason (in my case the view is an optional 'featured' checkbox) then this argument code breaks. I found it necessary to add at least one key of node id 0 to prevent the implode from breaking and the sql from becoming invalid... like this:
$exclude = views_get_view('sticky');
$exclude->args = $view->args;
$exclude->execute();
$nids = array(0 => 0); //provide a default argument of node 0 in case the view is empty
foreach ($exclude->result as $node) {
$nids[] = $node->nid;
}
return implode(',', $nids);
There might be a more efficient way to do this.
thanks for the help
In Views 2 our approach should be quite different
You can simply make two "displays" of the same View, and tell one of the lists to have the first values excluded.
You do this by clicking on the number in:
And below that you will get the option not just to set the items per page (or block), but to provide an offset.
(I'm fairly certain. Haven't actually done it yet.)
Not sure of your idea here, but very interested.
Hi. I'm really keen to know if there is a cleaner way via the standard views interface and options. The offset you mention I have already used on this same site. It has allowed me to have two displays of the same view but where the first view returns the first two stories and the second view returns the rest via setting the offset you mention to 2.
What I can't work out from playing with this is how this could be used to actually exclude the results of an alternate display (or view for that matter), without relying on the PHP exclude approach. Certainly would be interested to know if there is a cleaner way of us doing this than what we have above.
many thanks for the help.
A similar approach could be used to provide a backup view
Assign two views to a block.
If the first view has no output, then use the the next view.
You can tell the second view not to show up if the first view does have content by putting something like the above into the validation code:
<?php
$first_view = views_get_view('name_of_first_view');
// $first_view->args =
// if you provide a default argument, use the same PHP code there
$first_view->execute();
if (count($first_view)) {
return FALSE;
}
else {
return TRUE;
}
?>
Not tested.
Post new comment