Drupal 6: Using jQuery to pick up where views_calc falls short

Views Calc is a great Drupal module but there are a few things it doesn’t do.
It doesn’t:

  1. Accommodate totals by group
  2. Total CCK viewfield field values
  3. Total Views Customfield PHP values

I wanted to fix all three of those issue in a views report I’m using.
To be clear: My solution is uses jQuery as a hack/fix, specifically addressing the fields I want to count and sum. It does not address the inherent issue in views_calc.

First I’ll elaborate on what views_calc doesn’t do:

  1. Accommodate totals by group
    • views_calc always totals on the entire display’s result. If you group your results, the totals don’t change; the full totals are repeated under each group.
  2. Total CCK viewfield field values
    • The viewfield module allows you to embed a view display result into a cck field. Which is great. You can use that field in other view displays. But that result is ignored by views_calc.
  3. Total Views Customfield PHP values
    • views_customfield php values are a free for all, you could return any data you want, in any format so again views_calc ignores those.

In case you’re curious the reason I’m using viewfield and views_customfield is to calculated the difference between two dates and display the number of hours.

Here’s a look at what the last two groups in my report look like without the jQuery hack/fix:
Before:
The views display using views_calc showing is shortcomings
You can see that the count and total shown is repeated in each group. In addition we should be seeing a total on the Hours Calculated column.


And here’s what it looks like with the fix:
After:
Using jQuery to insert and update accurate counts and sums
Now you can see individual totals.


Here’s the core of the code I put together to total my columns. This I tested in the firebug console:

console.log(jQuery('.view-odt-activity-tracker-hrs-calc .views-row .views-field-phpcode .field-content').size());
var hourSum = parseFloat(0), enrollmentSum = parseFloat(0), countTitles = parseInt(0);
//hours
$('.view-odt-activity-tracker-report .views-table').each( function() {
  hourSum = 0;
  enrollmentSum = 0;
  countTitles = 0;
  //hours
  $('.views-row .views-field-phpcode span.field-content', this).each(
    function(){
      hourSum += parseFloat($(this).html());
      console.log($(this).html());
      console.log('hourSum: '+hourSum);
    }
  )
  console.log('hourSum: '+hourSum);
  $('tr.view-footer-number:contains("Total SUM") .views-field-field-odt-act-delivery-hrs-calc-vname', this).html(hourSum);

  //enrollment
  //$('tr:not(.view-footer-number) td.views-field-field-odt-act-enrollment-value', this).length;
  $('tr:not(.view-footer-number) td.views-field-field-odt-act-enrollment-value', this).each(
    function(){
      enrollmentSum += parseFloat($(this).html());
      console.log('enrollment html'+$(this).html());
      console.log('enrollmentSum: '+enrollmentSum);
    }
  )
  console.log('enrollmentSum: '+enrollmentSum);
  $('tr.view-footer-number:contains("Total SUM") .views-field-field-odt-act-enrollment-value', this).html(enrollmentSum);

  countTitles = $('tr:not(.view-footer_number) td.views-field-title a', this).length
  //count title links
  $('tr.view-footer-number:contains("Total COUNT") .views-field-title', this).html(countTitles);
  console.log('count titles: '+countTitles);
});

Note: The console.log statements are for firebug.
Steps:

  1. I start by declaring some global vars.
  2. Then I loop through the view tables, specifically the ones in my display: ‘view-odt-activity-tracker-report’ like so:
    $('.view-odt-activity-tracker-report .views-table').each()
  3. Reset the global vars
  4. Loop through the nested table cells.
    • ‘this’ is the awesomeness of jQuery at work; in the ‘each’ loop you have access to ‘this’ and $(this). Which is great for eliminating redundant code but fantastic for nested loops!
      In this case I use a jQuery selector that isolates the span tag inside the current table we’re looping through. To do this we create a selector as you normally would and add ‘this’ separated by a comma, like so:
      $('.views-row .views-field-phpcode span.field-content', this)...
    • Now I retrieve the value of the cell and add it to the global value. This is done with another each() loop with may be unnecessary; but it worked for a clean way to isolate the value and cast it as a float like this:
      parseFloat($(this).html());
    • All that’s left to do is update the correct ‘Total’ cell with the global value. In my case I isolate the total cell in question by checking for the total cell’s class in a row in the table’s footer that contains “Total SUM” like so:
      $('tr.view-footer-number:contains("Total SUM") .views-field-field-odt-act-delivery-hrs-calc-vname', this).html(hourSum);
  5. I repeat that process for the another column. The 2nd one is a little simpler because it doesn’t use a viewfield (less markup).
  6. Then I nearly pull a one-liner for the count. Normal row numbering didn’t work in this view and views_calc count only works in non-grouped displays. So this method worked great here. It’s actually 2 lines but I separated out the value in to a variable so I could push it to the console.log
    • I set the value of my count var: “countTitles” using some fantastic ‘not’ selectors (thank you jQuery)
    • Then I update the html value of the ‘Total COUNT’ cell with that result.

That was in testing.

In production, I wrapped the code in Drupal.behaviors and wrapped that in a php HEREDOC to leverage drupal_add_js. Then I add this code to the footer of the views display using the php input format.

Here’s the full code:

<?php
//odt.uoregon.edu-admin-build-views-edit-odt_activity_tracker_report-views-tab-page_4-footer
########## Add JS ############
$sumTrackerHoursDetailed = <<<javascript
/* ######################## extend jquery ######################## */
Drupal.behaviors.sumTrackerHoursDetailed=function(context){
  //run only if there is a context var
  if(typeof context !== 'undefined'){
    /*
      Check the type of the context var and type. Process if context = &#91;object HTMLDocument&#93;
      Note: when context == &#91;object HTMLDocument&#93; then this is page load. You may wont some script to run only on page load. Some not and some all the time.
    */
    //if(typeof context == 'object' && context.toString().indexOf('HTMLDocument')!=-1){ //page load
      /* ###################### Custom Code ###################### */

      //console.log(jQuery('.view-odt-activity-tracker-hrs-calc .views-row .views-field-phpcode .field-content').size());
      var hourSum = parseFloat(0), enrollmentSum = parseFloat(0), countTitles = parseInt(0);
      //hours
      $('.view-odt-activity-tracker-report .views-table').each( function() {
        hourSum = 0;
        enrollmentSum = 0;
        countTitles = 0;
        //hours
        $('.views-row .views-field-phpcode span.field-content', this).each(
          function(){
            hourSum += parseFloat($(this).html());
            //console.log($(this).html());
            //console.log('hourSum: '+hourSum);
          }
        )
        //console.log('hourSum: '+hourSum);
        $('tr.view-footer-number:contains("Total SUM") .views-field-field-odt-act-delivery-hrs-calc-vname', this).html(hourSum);

        //enrollment
        //$('tr:not(.view-footer-number) td.views-field-field-odt-act-enrollment-value', this).length;
        $('tr:not(.view-footer-number) td.views-field-field-odt-act-enrollment-value', this).each(
          function(){
            enrollmentSum += parseFloat($(this).html());
            //console.log('enrollment html'+$(this).html());
            //console.log('enrollmentSum: '+enrollmentSum);
          }
        )
        //console.log('enrollmentSum: '+enrollmentSum);
        $('tr.view-footer-number:contains("Total SUM") .views-field-field-odt-act-enrollment-value', this).html(enrollmentSum);

        countTitles = $('tr:not(.view-footer_number) td.views-field-title a', this).length
        //count title links
        $('tr.view-footer-number:contains("Total COUNT") .views-field-title', this).html(countTitles);
        console.log('count titles: '+countTitles);
      });

      /* ###################### End Custom Code ###################### */
    //}//end pageload
  }//end typeof context !== und
}//end Drupal.behaviors

/* ######################## end extend jquery ######################## */
JAVASCRIPT;
drupal_add_js($sumTrackerHoursDetailed, 'inline');
########## END Add JS ############
?>

If you’re looking close you’ll see the “page load” if statement commented out:

//if(typeof context == 'object' && context.toString().indexOf('HTMLDocument')!=-1){ //page load

Initially I was only running this on page load but when I enabled filtering I realized I need this to run every time the totals are re-calculated.

And that’s it!
Rocking awesome reports with views calc and jQuery.

Leave a Comment