Blogs

Posted by Eddie

A great thing about Drupal is that you can extend the functionality of a site without manipulating the core code upon which the software is built.

A situation arose recently where a client wanted to change the behavior of the core comment module. They didn't say as much, but that was what needed to be done in order to achieve the desired outcome.

So the comment module allows anonymous users to leave comments when permissions are set for that. The anonymous user may leave their name in the name field instead of the default value 'anonymous'. This becomes a problem, however, when the name they enter is an exact match of an existing registered user on the site.

So, say the registered user's name is 'sam' and the anonymous user's name is 'sam', the anonymous user cannot leave their name as such because Drupal won't allow it.

So I built a module I'm calling 'comment display name'. This essentially runs a check to determine if the user is anonymous, and if it is, then defaults the required name field to 'anonymous' but hides it. It uses new database tables to store a 'display name', which the user will be entering in place of the original name field (which is now hidden). This new field doesn't show up for registered users, nor does it change any core behavior.

Here is the module for download if you'd like to try it out on Drupal 6.

Use Case:

1) You want anonymous users to comment
2) You have registered users on your site
3) You want to avoid name conflicts between registered users and anonymous users.

Continue reading...
Posted by Eddie

Here is a snippet that will allow you to configure the NextGen gallery plugin as an ad rotator. A client requested this functionality and this is the way I went about it. I chose NextGen because of it's ease of use for the client.

The javascript below extracts the description of the image, which the client will use as a destination URL, and supplants that as the new hyperlink. It rotates through each image in the gallery every 5 seconds. When it gets to the end, it starts over.

 

<script type="text/javascript">
var x = 0;
var length = jQuery('.ngg-gallery-thumbnail').size();

function countUp() {
	if (x < (length - 1)) { 
		x = x + 1
	} else {
	x = 0
	}
} 

function showImage() { 
	countUp();
	jQuery('.ngg-galleryoverview img').css({'display' : 'none'});
	jQuery('.ngg-galleryoverview span').css({'display' : 'none'});	
	jQuery('.ngg-galleryoverview img:eq(' + x + ')').show();
	var description = jQuery('.ngg-galleryoverview span:eq(' + x + ')').html();
	var href = jQuery('.ngg-galleryoverview img:eq(' + x + ')').parent().attr({'href' : description});
}

jQuery(document).ready(function() {
	showImage();
});

setInterval('showImage()', 5000);

</script>

The next step is to embed this into the template.

That's as easy as

 
<?php 
echo do_shortcode('[nggallery id=1 template=caption]');
?>

Here is a live example, located on the right sidebar.

Continue reading...
Posted by Eddie

I recently helped The Kloud Agency with a new Wordpress site. On one of the artist pages, they needed a custom pagination solution to thumb through the artists' works. If you click the artist page, you'll see six thumbnail images, with pagination options below.

Here's how I went about creating that solution.


<div id="pagination-container">
		<div id="backward">Previous</div>
		<div id="pagination">Displaying <span id="low"></span> - <span id="high"></span> of <span id="total"></span></div>

		<div id="forward">Next</div>
	</div>

This code above creates the pagination HTML. It starts out initially empty and when the DOM is ready, the values are updated using the javascript below.

 	 	<script type="text/javascript">
		var imagesVisible = 6;
		var totalImages = jQuery('.gallery dl').size();		
		var totalPages = Math.floor(totalImages / imagesVisible);
		var lowBase =0;
		var highBase= imagesVisible;
		var size = jQuery('.gallery-item').size();

		function updatePagination() {
			jQuery("span#low").html(lowBase + 1);
			if(highBase <= size) {
				jQuery("span#high").html(highBase);
			} else {
				jQuery("span#high").html(size);
			}
			jQuery("span#total").html(size);
		}

 	
		jQuery(document).ready(function() {
			jQuery('.gallery br').remove();
			
			jQuery('<br style="clear:both"/>').appendTo('#artist_thumbnails');
			
			jQuery('.gallery-item').css({'display':'none'});
			jQuery('.gallery-item:lt(' + highBase + ')').css({'display':'block'});			

			jQuery('#backward').css({'display':'none'});
		});

		jQuery('#backward').click(function(){
			
			lowBase = lowBase - imagesVisible; 	
			highBase = highBase - imagesVisible;
			jQuery('.gallery-item').css({'display':'none'});
			if (lowBase != 0) {
			jQuery('.gallery-item:gt(' + (lowBase - 1) + ')').css({'display':'block'});
			} else {
				jQuery('.gallery-item:lt(' + (highBase + 1) + ')').css({'display':'block'});
     		}
			
			jQuery('.gallery-item:gt(' + (highBase - 1) + ')').css({'display':'none'});			
			if (lowBase > 1) {
				jQuery('#forward').css({'display' : 'inline' });
			}
			
			if (lowBase <= 1) {
				jQuery('#backward').css({'display':'none'});
			}
			updatePagination();
		});


		jQuery('#forward').click(function(){
			jQuery('#backward').css({'display':'inline'});
			lowBase = lowBase + imagesVisible; 	
			highBase = highBase + imagesVisible;
			jQuery('.gallery-item').css({'display':'none'});
			jQuery('.gallery-item:gt(' + (lowBase - 1) + ')').css({'display':'block'});
			jQuery('.gallery-item:gt(' + (highBase - 1) + ')').css({'display':'none'});			
			if (highBase > size) {
				jQuery('#forward').css({'display' : 'none' });
			}
			updatePagination();
		});

		updatePagination();

 	</script>	

So a bit about the script. You can change the imagesVisible variable to whichever value makes the most sense for you. Assuming you are starting with a list or container of images, you need to determine the total amount of images. For that, we use jQuery's size() to count our total images. Now we can determine how many pages of images we'll have by dividing the number of total images by the amount of images visible we want and rounding down.

There are a few more functions to handle forward and backward traversal through the gallery.

Continue reading...
Posted by Eddie

The following code shows how I went about limiting core drupal search results to two content types, and then grouping content from those types together. In this case I stacked the content types so all of content type a would show first, followed by all content from content type b and so on. You could display the results side by side or whatever. This was done at the theme level in search-results.tpl.php


<?php 
$i=count($results);
if ($i > 0) { ?>
<h1>Search Results</h1>
<h3>You searched for <span id="query"></span></h3>
<?php } ?>

<?php print $keys; 


 for($x=0;$x<=$i;$x++){ 
	if ($results[$x]['node']->type == 'product') { ?>
	    <div class="search-box">
	        <div class="search-image">
				<img src="/<?php print $results[$x]['node']->field_productimage[0]['filepath']?>"/>
	        </div>
	        <div class="search-text">
				<div class="product-headline">
					<div class="product-headline-title">
						<a href="/node/<?php print $results[$x]['node']->nid;?>"><?php print $results[$x]['node']->field_productname[0]['safe'];?></a>
					</div>
					<div class="product-code">
						<?php print $results[$x]['node']->field_productcode[0]['value'];?>
					</div>
				</div>
				<div class="product-body">
					<?php if ($results[$x]['node']->content['body']['#value'] != NULL) { 
						print $results[$x]['node']->content['body']['#value'];
						} else {
						print 'Product information coming soon';
						} ?>
						<div class="product-details"><a href="/node/<?php print $results[$x]['node']->nid;?>">View Product Details</a></div>
				</div>
				
			</div>
		</div>
	<?php 	}
} ?>
<?php
 for($x=0;$x<=$i;$x++){ 
	if ($results[$x]['node']->type == 'animal') { ?>
	<div class="search-box animal">
		<div class="search-image">
			<img src="/<?php print $results[$x]['node']->field_thumbnail_photo[0]['filepath']?>"/>
		</div>
		<div class="search-text">
				<div class="product-headline">
					<div class="product-headline-title">
						<a href="/animals/<?php print $results[$x]['node']->title;?>"><?php print $results[$x]['node']->title;?> Care Info and FAQs</a>
					</div>

				</div>
				<div class="product-body">
					Find out more about this animal and the care needed
					<div class="product-details"><a href="/animals/<?php print $results[$x]['node']->title;?>">View <?php print $results[$x]['node']->title;?> Information</a></div>
				</div>
		</div>
	</div>
<?php	
	}
}


?>




</dl>
<?php print $pager; ?>

<script type="text/javascript">
$(document).ready(function() {
    var query = $('#edit-keys-wrapper input.form-text').val();
	$('#query').replaceWith('<span id="query">' + query + '</span>');
});

</script>

The jQuery at the end basically is just a quick way of showing the user what they searched for without having to split atoms in template.php

Posted by Eddie

One of the sites I'm working on needs a photo gallery consisting of about 300 images. However, the images they need in the gallery are stored on their old server which we don't readily have access to. So rather than copying each photo by hand, we can use PHP to copy them directly to our server without having to move each one manually. Here is a script I wrote yesterday that saved me a ton of time.

Once again, I opted to use the simple_html_dom parser, which uses selectors like jQuery to match elements on a page and allows one to do fun stuff to manipulate the data.

On the site I need to take the images from, they are listed like this:

<a href='full/photo055.jpg' rel='lightbox' style='color:#ffffff;'><img src='thumbs/photo055.jpg' border='0' width='100' height='100'>	055	</a>
<a href='full/photo294.jpg' rel='lightbox' style='color:#ffffff;'><img src='thumbs/photo294.jpg' border='0' width='100' height='100'>	294	</a>
...

And so on about 300 more times. It's a thumbnail image linked to a larger image that appears in a lightbox when the user clicks. All we're interested in, really, is grabbing the large image and copying to our server.

So here's a script that parses this page and matches each <a> tag. It examines each match again, this time only using matches that end with 'jpg'. Then each of those matches are striped of extraneous code, starting with <a href='full/photo055.jpg' rel='lightbox' style='color:#ffffff;'><img src='thumbs/photo055.jpg' border='0' width='100' height='100'> 055 </a> and whittling down to just 'full/photo055.jpg'. Once that's done, we can add on a new path of where we want the copied photo to live on our server.

<?php 
include ('includes/simple_html_dom.php');
$html = file_get_html('http://www.somedomain.com/photos/index.html');
$frontremove = "<a href='";
$backremove = "' rel='lightbox' style='color:#ffffff;'>";
$x=0;
foreach($html->find('a') as $element) {
	if(preg_match('/jpg/',$element)) {
		$image[$x] = str_replace($frontremove,"",$element);
		$image[$x] = str_replace($backremove,"",$image[$x]);
		$image[$x] = substr($image[$x], 0, 17);
		$x++;
	}
}
for($n=0;$n<=$x;$n++) {
	$file = 'http://www.somedomain.com/photos/'.$image[$n];
	$newfile = $_SERVER['DOCUMENT_ROOT'] . '/photos/'.$image[$n];
	echo $newfile;
	fopen($newfile, "w");
	if (!copy($file, $newfile)) {
		echo "failed to copy $file...\n";
	}
	fclose($newfile);
}
?>

Now that I'm looking at this script again under a well-rested mindset, I think I could simplify this and avoid using DOM parsing altogether since we know the remote directory. This simpler method would save memory and probably work more efficiently. I probably overcomplicated this script.

However, an advantage to using DOM parsing is that you would only copy over the files that you want from the page, rather than running the risk of copying any other files that may be living in that directory that you may not want.

Oh well, it's all a learning experience. I'll play with a simplified version later.

Posted by Eddie

This post shows you how to render the markup needed for an animated drop down menu in Wordpress based on your nested parent/child page hierarchy using PHP.

OK, first let's see an example in action of what exactly we are trying to do here. The way wp_list_pages() renders the menu list items needs to be rewritten in order to power the structure of this menu:

Dynamic Menu Demonstration

Click on the link above and interact with the menu. That should do some pretty interesting things. It has a nice info area when you mouse over the menu items.

Now the need arises to be able to manage the content of this menu without having to edit the code in the template. The wp_list_pages() template tag has some pretty nice parameters to work with, but in this case, it really isn't robust enough to render the necessary markup for this menu as is. So I had to write a script to explode wp_list_pages and check each menu item for possible submenu items based on the page structure define by the administrator in the wordpress administrative panel:

<div id="quicklinks">
  <ul class="kwicks horizontal" >
    <?php $toppages = wp_list_pages('echo=0&title_li=&depth=1');
        $menuitem = explode('<li class',$toppages); 
        $n = count($menuitem);
        for ($i = 1;$i<$n;$i++){
            preg_match('/page-item-[0-9]*/', $menuitem[$i],$id); 
            $id2 = str_replace("page-item-","",$id);
            $x = $id2[0];
            $submenu = wp_list_pages('echo=0&title_li=&depth=2&child_of='.$x);
            echo '<li id="kwick_'.$i.'">
					<div class="wrapper">
						<div class="col1"><h3><li class'.$menuitem[$i].'</h3>
						<ul class="menu2">'.$submenu.'</ul></div> 
						 <div id="info_'.$x.'" class="col2" style="display: none;"> This is an info area. </div>
					</div>
				     </li>';
	     
        }
     ?> 
  </ul>
</div>

The script basically takes the list of pages, explodes it into pieces delineated by the <li> tag, counts the number of pieces, then loops through each item, checking it to see if there are subpages for that menu item by determining the page ID, running wp_list_pages() again and rendering those subpages underneath - all with the proper markup needed for the menu to work.

This was a bit of a challenge, but a great exercise in PHP. Using the script above could likely be adapted and improved to whatever you would need for a unique menu of your own powered by the Wordpress paging system.

I'm sure there are plugins for drop downs - sometimes it's more rewarding to build it yourself.

Posted by Eddie

These are the tags assigned to a sample post. The tag "sonoma" appears slightly larger than the other tags assigned to this post because it appears more frequently throughout all other posts.

A recent development project called for a tag cloud on the right rail. Easy to do generally; drupal has plenty of taxonomy modules available to take care of this for the most part.

What I needed that was different than any existing functionality I found was a tag cloud that showed:

1) A list of the node's terms in a block. These would be terms assigned to this post rather than an entire vocabulary.
2) The relative weights of those terms (and displayed visually using font size) as in a typical tag cloud.

The challenge was combining parts one and two. If you needed to apply this, the only thing in theory you'd need to change would be the vid = 5 in the database query to what ever vocabulary ID in which you want to run your query. So here is some code that does the trick (it could be simplified I think):


<?php
class tag
{
    var $name = "";
    var $count = 0;
    var $tid = 0;

    function getCount()
    {
        return $this->count;
    }
    function getName()
    {
        return $this->name;
    }
    function setCount($var)
    {
        $this->count = $var;
    }
    function setName($var)
    {
        $this->name = $var;
    }
    function setTID($var)
    {
        $this->tid = $var;
    }
    function getTID()
    {
        return $this->tid;
    }
}
$count = 0; // Overall count of used tags
$threshold = 0; // How many tags are needed to get displayed
$font_size = 0.8;
$node = node_load(arg(1));
$nid = $node->nid;
$query = "SELECT d.name,d.tid, dn.nid FROM {term_data} d, {term_node} dn WHERE dn.tid=d.tid and d.vid='5';";
$result = db_query($query);
$tags['Test'] = 0;
while($node = db_fetch_array($result))
{
    
    if ($tags[$node['name']] == NULL)
    {
        $tags[$node['name']] = new tag();
        $tags[$node['name']]->setName($node['name']);
        $tags[$node['name']]->setCount(1);
        $tags[$node['name']]->setTID($node['tid']);
        $count = $count + 1;
    }
    else
    {
        $tags[$node['name']]->setCount(
            $tags[$node['name']]->getCount() + 1
        );
        $count = $count + 1;
    }
}

$query2 = "SELECT d.name,d.tid, dn.nid FROM {term_data} d, {term_node} dn WHERE dn.tid=d.tid and dn.nid = '$nid' and d.vid='5';";
$result2 = db_query($query2);
$match1 =  $node['name'];
$match2 = $tag->name;
	while($node = db_fetch_array($result2)) {
			$termnames[] = $node['name'];
	}

foreach($tags as $tag)
{
    foreach($termnames as $termname){              
                 if($tag->name == $termname){
                 $matches[] = $tag; 
				}	
		}
}

foreach($matches as $match)
 {
    $mycount = $match->count;

    if($mycount > $threshold) 

    {
        $fraction = ((int)(($mycount / $count) * 60)) / 10;
        if($fraction < $font_size)
        {
            $fraction = $font_size;
        }
        echo '<span style="font-size: ' . $fraction . 'em;">' . l($match->name,'taxonomy/term/' . $match->tid,array('title="' . $match->count . ' Nodes"')).'</span> ';
    }
}

?>

The first half of the code I lifted from a snippet or forum post on Drupal.org many months ago, and the second half I managed to cobble together somehow on a cloudy day when the code gods were looking the other way.

Continue reading...
Posted by Eddie

Though I personally loathe twitter because of the distraction it breeds, it has a very powerful API which allows developers to create cool apps like twhirl or tweetdeck. Here is one that runs in a browser.

Background:

The client has a website built on a Drupal 6 platform and needs users to be able to tweet to their account from his Website. The tweet box on his Web site has a sports hashtag already entered into the tweetbox, and entries with that hashtag will flow through the real-time stream of that sports category. The real time twitter stream is shown to the left of the tweet box on the homepage of the site, so users can see their tweet go through the twitter river in fairly short order.

I tried a few drupal modules, but they just aren't there yet to both allow users to sign in from twitter AND tweet from the drupal site. The twitter 6.x-3.x module is still in development and I don't know enough about module development to try to offer a patch to make it work for this purpose. This module does allow you to sign in with twitter, but I was unsuccessful in getting a tweet to post using it. The official release, 6.x-2.6 doesn't have oauth support, so that isn't possible for this task.

Application overview:

This application leverages a PHP code library developed by Jaison Mathai. In fact the library pretty much is the app, with a few tweaks for my own purposes.

The application uses oauth to let twitter handle all of the login and authentication stuff, so users don't have to enter in their password to anyone but twitter. Twitter then sends back a token to requesting website, which then sets a cookie in the user's browser. The user, with cookie stored now, can now tweet in the box on the site. No passwords are ever stored - just an authentication token that expires after you log out of twitter.

For more on the behavior and security of oauth, read about it here.

Here is the very cool code library I used.

Demonstration

If you'd like to test this out how it works, go ahead and authenticate my site by signing in with Twitter
.

Once that's done, you can tweet into this box here:


Extending the Application?

The Drupal twitter module is very powerful and hooks into the core user module to create a user account for someone signing into the drupal site using twitter sign in. However, what if you don't need or want to create a user account from every authorization? What if the goal is simply to allow users to tweet and not create an account on your site?

I just picked up a copy of Pro Drupal Development, and I'm finding it very resourceful and full of great knowledge. I have yet to wrap my head around building modules for Drupal and perhaps this would be a good exercise to turn this application into a plug-and-play module, even if it isn't committed back to drupal.org.

Continue reading...
Posted by Eddie

Amid South Carolina Gov. Mark Sanford's newsmaking affair with an Argentine woman last week, we decided to run an online poll asking our readers whether Sanford should resign. We used a widget developed by Vizu to run our unscientific poll. I like their service because it's very fast, fairly customizable and produces a cool Google map of voters' locations.

This was working great as is, until the top editor of the paper said we need to show the vote totals on the site. Well, that info doesn't come as part of the Vizu widget directly, but it is visible on the page where the poll was created - but that's on Vizu's site. On a fast-moving news day (or any other day), the last thing you want to do is update anything every fifteen minutes by hand when you don't have to.

Enter Simple_HMTL_Dom - a PHP parser that does what is says - it simply parses HTML from the DOM of a given Web page. In plainer English - it scrapes a Web page and allows you to extract the information you need.

Here is the end result of what I was trying to achieve:

Poll: Should Sanford resign?

 Click here to see map of votes

That vote total above is generated by a javascript which was generated using PHP. A cron hits the PHP script every 15 minutes, which in turn writes the result to javascript. The reason for that is to keep server hits to Vizu to a minimum. It also lightens the load on your own server because you're only serving a few lines of code and since we're using drupal to write the script, doesn't hit the MYSQL database in the process.

In any event, notice the Vizu page where this poll was created. It's here:

http://www.vizu.com//poll-results.html?n=170479

Notice just above the bar chart you'll see the vote totals. That's the information we want to show dynamically.

There's really not much to Simple_HTML_Dom. Just download the script and save it to your server, include() it in a PHP script and rock and roll.

Here you go:

<?php 
include ('simple_html_dom.php');
$html = file_get_html('http://www.vizu.com/poll-results.html?n=170479');
$es = $html->find('td[align=right]');
$votes = "document.write(".$es[0].");";
$FileName2 = "votecount.js";
$FileHandle2 = fopen($FileName2, 'w') or die("can't open file");
fwrite($FileHandle2, $votes);
fclose($FileHandle2);
?>

So a few things here. The line that says

$es = $html->find('td[align=right]'); 

is just looking at the source code of the Vizu page and matching elements it finds. In this case, the first match is the Vote total. This is signified in the next line, when we define the one line of javascript that will right the first matched element:

$votes = "document.write(".$es[0].");";

Finally, we write it to a file (don't forget to first create the blank file and save it with the correct read/write/execute permissions).

The last thing you'd want to do, is set up a cron job to hit the URL of the php script every so often - in our case 15 minutes. There is tons of documentation on this step and I'll leave you to google to sort that out.

Now, in the HTML of the page where you want the dynamic info to show up, it's simply:

<script src="http://blogs.islandpacket.com/sites/default/files/votecount.js?1" type="text/javascript">

There you have it. Once you've done this once or twice, it doesn't take long to do something like this on the fly under pressure.

Continue reading...
Tags: |  
Posted by Eddie

I was recently trying to figure out how to get certain categories of staff blog posts, run on drupal 6, out of the database and written to a javascript so they could be embedded on our main site, with the headline, content category and post date intact.

I started out going the PHP db_query() function route - which uses SQL and is frankly pretty intimidating to me. For those unfamiliar, it looks something like this:

$tidQuery = db_query('SELECT td.name FROM{term_node}tn, {term_data}td, {term_hierarchy}th WHERE tn.tid = td.tid AND tn.tid = th.tid AND th.parent = 0 AND tn.nid = $Row->nid');

This would then be looped and written to a javascript in some more code that I won't bother posting.

Everytime something needed to be tweaked, the database query would have to be modified, and I'd spend time trying to figure out which database table needed to match.

Um...while structured query language is starting to make more sense to me, and is something I'd like to learn, a faster method of getting the desired result was to leverage Drupal Views and the powerful templating system to output the views query as a javascript.

So again - the ultimate result of this to use drupal views module to produce an embeddable widget for other sites.

Step 1 - In your view, create a page and assign it a path that you'll use specifically for the purpose. I called mine myviewname/custom.js. Then, in the page.tpl.php file, use an if statement to determine if the URL ends with "custom.js". This is done using arguments and should look something like this:


<?php if(arg(1) !== 'custom.js') { ?>
STANDARD PAGE TEMPLATE HERE
<?php } else {
print $content;
} ?>

The last part tells us that the only thing needed for the URL ending in custom.js is the content (which the view will return when we built it)

Step 2 - In my view, I configured it to use fields of exactly what I want, and I created three new tpl.php files to alter the output of the view - stripping away even further the html wrappers and content unnecessary for this purpose and replacing it with a javascript document.write() wrapper.

For example, in the new view-view-fields.tpl.php I added to my theme folder, all I have is :

<?php foreach ($fields as $id => $field): ?>
document.write('<<?php print $field->inline_html;?>><?php print $field->content; ?>inline_html;?>>');
<?php endforeach; ?>

My view looks like this on the backend:

The other template files I created were more specific versions of

view-view.tpl.php

view-view-unformatted.tpl.php

Step 3 - So now we have a view that spits out its output like this:


document.write('
'); document.write('
USCB men's golf team awaits word on nationals
'); document.write('Sharkbites | '); document.write('May 8, 2009'); document.write('
'); document.write('
'); document.write('
Beaufort LB Justin Parker cracks Sporting News Today's list of top recruits
'); document.write('Footblog | '); document.write('May 7, 2009'); document.write('
'); document.write('
'); document.write('
Does your dog look like an Ewok? He could be a winner Saturday
'); document.write('The Pack | '); document.write('May 7, 2009'); document.write('
'); document.write('
'); document.write('
Bluffton continues search for new town manager
'); document.write('Bluffton Blog | '); document.write('May 7, 2009'); document.write('
');

So yeah, there are a few too many document.write() statements in there, but the result is the same.

What we want to do to keep our database hits to a minimum, is prevent views from running the db query everytime our main site is hit, so we'll write this view to another javascript, which will then be included on the main site. I created a drupal page to do that, using this php code to write it to file:


$o = file_get_contents('custom.js');
$FileName = "files/blogfeed.js";
$FileHandle = fopen($FileName, 'w') or die("can't open file");
fwrite($FileHandle, $o);
fclose($FileHandle);

A cron job then hits this new page every 10 minutes, and writes it to file. The result is a low-impact, view-created javascript widget of the latest posts that we can use on any other site.

There is probably a better way to do all of this. But this way just sort of clicked for me as something cool you can do with views and templating to export customized data out of drupal.

Continue reading...
Tags: |