Generic date group; group by day, hour, week and other date granularities

Bevan - September 25, 2007 - 00:44
Project:Views 'Group-By' Pack
Version:5.x-1.x-dev
Component:Code
Category:feature request
Priority:normal
Assigned:Unassigned
Status:postponed (maintainer needs more info)
Description

I think I've worked out an approach that might make turning this into a generic date group module grok-able;

Add a settings page where users can add groupings. Groupings are a definition of how to group and label a views_style_plugin. They have
* a set of active date granularities, e.g. 'Month and Year' (as month group currently implements), 'Day and Week'. The interface would be checkboxes for Second, Minute, Hour, Day, Week, Month, Year. (Later this could expand to allow integers to be set, instead of just on off. This would mean a fortnightly grouping could be obtained by setting Week=2, and all the others to zero.)
* title of each date granularity using tokens and date(), e.g. "\Day \[count], l D \of F, Y" for "Day 1, Monday 24 of September, 2007" followed by "Day 2, Tuesday 25 of September, 2007" etc.

These settings are stored with variable_set() or similar, and called whenever views.module invokes monthgroup_views_style_plugins() to return the currently set groupings as plugins.

The callback function, would then sort the nodes using the following recursive algorithm;

  1. Find the largest date granularity that is not sorted (e.g. nodes aren't yet sorted by month, so 'month' is the result)
  2. Sort the nodes by this date granularity
  3. For each group of nodes sort them by group using this procedure (recursively), e.g.
    1. Find the largest date granularity that is not sorted (e.g. could be 'day' this time)
    2. Sort the nodes by this date granularity
    3. For each group of nodes sort them by group using this procedure (recursively)

After recursion completes, we would have a tree (array of arrays of arrays...) of nodes that is exactly as many levels deep as active date granularities set in the grouping definition with the highest level (below root) being the largest granularity (probably year or month), and the lowest level being the smallest granularity. Each terminal of the tree is on the same level (the lowest level), and is a drupal $node object.

Theming is then relatively trivial, as the tree exactly represents a the DOM tree of divs we must build. It would be best to hand this layer off to another theme function (theme_monthgroup_nodetree()?) so that themers don't have to copy (and fork) the code for the above algorithm. This is also separating logic from interface better.

The only thing tricky thing left for that theme function is the title, as defined for each granularity in the grouping settings. We could key the arrays by title, which would be convenient, but as the title is set by the user and can be any string with an unknown amount of uniqueness, it could cause nested arrays to merge if titles are not unique. We could also perhaps do the title in the sort algorithm. But that would mean complicating the tree structure by having extra unecessary data types in the tree and an irregular shape.

Perhaps we could just handoff the theming of each group's title to theme_monthgroup_title() along with all the parameters it needs to work out which title string in the grouping it should use.

PHP / Pseudocode:

<?php
// Callback for view_style__plugin
function theme_monthgroup_monthgroup($view, $nodes, $type, $teaser = false) {
 
// Sort the nodes by date groups
 
$sorted_nodes = monthgroup_sort($view, $nodes, $type, $teaser);
 
 
// Hand off to be themed
 
return theme('monthgroup_nodetree', $view, $sorted_nodes, $type, $teaser);
}

// Recursively sort nodes into date groups
function monthgroup_sort($view, $unsorted_nodes, $type, $teaser, $grouping_level = 0) {
 
// This is where we'll store and return the root of the tree of date groups and drupal nodes (terminal tree nodes).
 
$sorted_nodes = array();

 
// Get the grouping from the $view object.
 
$grouping = _monthgroup_get_grouping_from_view($view);

 
// Get the date granularity of the date group in $grouping that we're dealing with now.
 
$date_granularity = monthgroup_get_granularity_from_view($view, $grouping_level);

 
// Get the ID of the datefield.module field that is sorting this $view
 
$datefield = _monthgroup_get_datefield_id_from_view($view);

 
// Sort the $unsorted nodes
 
foreach($unsorted_nodes as $node) {
   
// @todo: Check/fix the format of the datefield value as there are two different storage types of datefield: d.o node/178105
   
    // Which date group does this node belong to?
   
$node_granular_date_value = date($date_granularity, $node->$datefield[0]['value']);
   
   
// Put the node in it's date group in $sorted_nodes.
   
$sorted_nodes[$node_granular_date_value][] = $node;
  }

 
// Now recursively sort each date group of nodes in $sorted_nodes.
 
foreach ($sorted_nodes as $key => $unsorted_child_nodes) {

   
// Check if there are still grouping levels to group these nodes by.
   
if (isset($grouping[$grouping_level + 1])) {

     
// Replace the unsorted nodes with the sorted nodes
     
$sorted_nodes[$key] = monthgroup_sort($view, $unsorted_child_nodes, $type, $teaser, $grouping_level + 1);
    }
  }

  return
$sorted_nodes;
}

// Theme a node tree
function theme_monthgroup_nodetree($view, $node_tree, $type, $teaser, $grouping_level = 0) {
 
// Store themed nodes here
 
$themed_children = array();
 
 
// Theme the children and add them to the group of themed_child_nodes.
 
foreach ($node_tree as $child) {
    if (
is_array($child)) {
     
     
// The child is another monthgroup.
     
$themed_children[] = theme('monthgroup_nodetree', $view, $node_tree, $type, $teaser, $grouping_level + 1);
    }
    elseif (
isobject($child) && isset($child->nid) && $node = $child) {
     
     
// The child is a tree terminal node, and a drupal node.
     
$themed_children[] = theme('node', $teaser, false);
    }
  }

 
// Concatenate the HTML here
 
$output = '';
 
 
// Concatenate the group title and themed item list of the children
 
if (!empty($themed_child_nodes)) {
   
$output .= monthgroup_title($view, $node_tree, $grouping_level);
   
$granularity = monthgroup_get_granularity_from_view($view, $grouping_level);
   
$output .= theme('item_list', array('class' => 'monthgroup monthgroup-'. $granularity), 'ol');
  }
 
  return
$output;
}

// Return and theme a date group's title
function monthgroup_title($view, $grouping_level, $child_nodes) {
 
// @todo: add support for token substitution like [count], use tokens.module
 
if (is_object($child_nodes[0]) && $node = $child_nodes[0] && isset($node->nid)) {
   
$datefield = _monthgroup_get_datefield_id_from_view($view);
   
$grouping = monthgroup_get_grouping_from_view($view);
    return
theme('monthgroup_title', date($grouping[$grouping_level]['title'], $node->$datefield[0]['value']));
  }
}

// Theme a date group's title
function theme_monthgroup_title($title) {
  return
'<h2 clasee="title">'. $title .'</h2>';
}

/**
  * A grouping is just an array of date() character and date title pairs,
  * A valid grouping for a monthgroup_view_style_plugin grouped by week then day would be
array(
  0 => array(
    'g' => 'W',
    'title' => "\Week \[count] of Y"
  ),
  1 => array(
    'g' => 'z',
    "\Day \[count], l D \of F, Y"
  )
);
  */

// Return the granularity for a grouping at a level
function monthgroup_get_granularity_from_view($view, $grouping_level) {
 
$grouping = _monthgroup_get_grouping_from_view($view);
 
 
// The relationship between a grouping object and the granularity:
 
return $grouping[$grouping_level]['g'];
}

// Get the grouping from the $view object, and load it from settings
function monthgroup_get_grouping_from_view($view) {
 
// The relationship between persistence of grouping settings and persistence in drupal
  // This is probably not a good way to use variable_get
 
return variable_get('monthgroup_'. $view->style_plugin, null);  // @todo: $view->style_plugin is wrong -- this is just pseudocode, remember! :)
}

// Get the CCK datefield.module field that we are grouping by
function _monthgroup_get_datefield_id_from_view($view) {
 
// The relationship between a view and the field monthgroup uses to group nodes
 
return $view->sort[0]; // @todo: This is also probably wrong
}
?>

Thoughts, ideas, WDYT?

 
 

Drupal is a registered trademark of Dries Buytaert.