Exporting Drupal Content to Microsoft Word

I was recently given an interesting task at work. I was asked to export all blog posts for a given author from a Drupal site into a Microsoft Word document. At first, I wasn't sure how I was going to accomplish this, so I turned to Google and found a few PHP classes that purported to do exactly what I needed. However, a few false starts later, I was unable to get any of them to work. That's when I came across LiveDocX. LiveDocX is a template-based SaaS solution that allows developers to create documents from data across disparate data sources.
It allows developers to create word processing documents by combining user-defined Microsoft Word templates with data from disparate data sources, such as XML files and databases. It is typically used to create professional, print-ready word processing documents in DOCX, DOC, RTF and PDF. LiveDocx is a Web Service that can be easily integrated into any web application without installing or configuring any software on your server. Currently, the following programming languages are supported: * ASP.NET * PHP As LiveDocx is strictly based on open standards, it is simple to add support for more programming languages. As long as SOAP (Simple Object Access Protocol) is available on the client-side system, LiveDocx runs on all operating systems and in all programming languages.
This looked to be the best solution for what I was attempting to do, and best of all, it was free. All I had to do was sign up for an account, and then I was free to begin coding my solution. I knew that I wanted the solution to be dynamic; I didn't want to hard-code the author into my code. Instead, I wanted to be able to export any author's blog posts. So, step 1 was to create a form that would allow site administrators to find the author they are looking for. The form consists of a textfield with autocomplete functionality, and a select box with export options. For the purpose of this example, the only option is to export to MS Word (doc). However, LiveDocX also supports docx, rtf, and pdf.
<?php

function MYMODULE_blog_export_form() {
  
$form = array();
  
$form['export'] = array(
    
'#type' => 'fieldset',
    
'#title' => t('Blog Export Options'),
    
'#collapsed' => false,
    
'#collapsible' => false,
  );
  
$form['export']['method'] = array(
    
'#type' => 'select',
    
'#title' => t('Export output type'),
    
'#options' => array(
      
'msword' => t('Microsoft Word'),
    ),
  );
  
$form['export']['author'] = array(
    
'#type' => 'textfield',
    
'#title' => t('Author name'),
    
'#autocomplete_path' => 'admin/autocomplete/bloggers',
    
'#description' => t('Enter the name of the user.'),
  );
  
$form['submit'] = array(
    
'#type' => 'submit',
    
'#value' => t('Submit'),
  );
  return 
$form;
}

?>
You'll notice that the textfield has an #autocomplete property, which is the path that executes the autocomplete function. This function is defined like this:
<?php

/**
 * Menu callback function to provide autocomplete functionality for
 * searching for users by username
 *
 * @param string $search_string
 */
function MYMODULE_autocomplete_bloggers($search_string) {

  static 
$blogger_roles = array();
  
$result db_query("SELECT r.rid FROM {role} r WHERE
    r.name = '%s'"
'blogger');
  while(
$role db_fetch_object($result)) {
    
$blogger_roles[] = $role->rid;
  }
 
  
$matches = array();
  
$result db_query("SELECT u.uid, u.name FROM {users} u
    LEFT JOIN {users_roles} ur ON ur.uid = u.uid
    LEFT JOIN {role} r ON r.rid = ur.rid
    WHERE u.name LIKE '%s%%' AND
    r.rid IN ("
.join(','$blogger_roles).")
    LIMIT 50"
$search_string);
  while (
$row db_fetch_object($result)) {
    
$matches[$row->name] = $row->name;
  }
  print 
drupal_to_js($matches);
  exit();
}
?>
Now that the autocomplete functionality was hooked up, it was time to define the form's submit handler. The submit handler makes use of Drupal's Batch API to define a batch process and execute it.
<?php

function MYMODULE_blog_export_form_submit($form$form_state) {
  
$uid db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'"$form_state['values']['author']));
 
  if(
$uid) {
    
$start_func 'MYMODULE_blog_export_'.$form_state['values']['method'];
    
$finished_func 'MYMODULE_blog_export_'.$form_state['values']['method'].'_batch_process_finished';
 
    
// Add a batch set with simple operations taking an argument.
    
$batch = array(
      
'title' => t('Blog Export'), // Not displayed.
      
'operations' => array(
        array(
$start_func, array($uid)),
      ),
      
'finished' => $finished_func,
    );
    
batch_set($batch);
    
batch_process('admin/content/blogs/export');
  }
  else {
    
drupal_set_message('An error occurred while trying to process this action.');
  }  
}

?>
The above code defines a batch process with a start function, and an end function. For clarity, the start function is responsible for finding all of the nodes for the specified author. The end function is responsible for sending those results to LiveDocX.
<?php

function MYMODULE_blog_export_msword($uid, &$context) {
  
$limit 5;
  
$context['finished'] = 0;
  if (!isset(
$context['sandbox']['progress'])) {
    
$max_nodes db_result(db_query("SELECT count(n.nid) FROM {node} n WHERE n.uid = %d AND n.type = 'blog' ORDER BY nid ASC"$uid));
    
$context['sandbox']['progress'] = 0;
    
$context['sandbox']['current_node'] = 0;
    
$context['sandbox']['max'] = $max_nodes;
    
$context['sandbox']['results']['author'] = user_load(array('uid' => $uid));
    
$block_values = array();
    
$context['sandbox']['results']['block_values'] =& $block_values;   
  }
 
  
$nodes = array();
  
$result db_query_range("SELECT n.nid, n.type FROM {node} n WHERE n.nid > %d AND n.uid = %d AND n.type = 'blog' ORDER BY nid ASC"$context['sandbox']['current_node'], $uid0$limit);
  while(
$row db_fetch_object($result)) {
    
$nodes[$row->nid] = $row;
  }
  if(
count($nodes) == 0) {
    
cache_set('famed:blog_export_results''cache'serialize($context['sandbox']['results']));
    
$context['finished'] = 1;
  }
  
$context['message'] = t('Processing nodes authored by user %uid', array('%uid' => $uid));
 
  foreach (
$nodes as $node) {
    
// Process the node
    
$node node_load($node->nid);
    if(
$node) {
      
$content node_view($nodefalsetruefalse);
      if(
$content) {
        
$context['sandbox']['results']['block_values'][] = array (
          
'post_title' => $node->title,
          
'content' => strip_tags($node->body),
          
'created' => date('Y-m-d h:m:s'$node->created),
          
'updated' => date('Y-m-d h:m:s'$node->changed),
          
'pub_status' => ($node->status == 1) ? 'Published' 'Unpublished',
          
'tags' => $node->nodewords['keywords'],
        );
      }
    }
    
    
// Update our progress information.
    
$context['message'] = t('Processing blog posts authored by user %uid', array('%uid' => $uid));
    
$context['results'][] = t('Processed node %node', array('%node' => $node->nid));
    
$context['sandbox']['progress']++;
    
$context['sandbox']['current_node'] = $node->nid;
  }
    
  
// Inform the batch engine that we are not finished,
  // and provide an estimation of the completion level we reached.
  
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

?>
This code fetches all of the nodes from the database, and stores information about each node in the $context['sandbox']['results']['block_values'] array. When there are no more nodes to process, this array gets serialized and stored in Drupal's cache, so that the data can be used in the batch finished function. The finished function is responsible for sending all of the data to the LiveDocX web service. It's important that you create your document template before trying to send data to the web service, as LiveDocX works like a mail merge. Your template will consist of named MailMerge fields, and merge blocks for repeating data.
<?php

/**
 * Batch finished handler.
 */
function MYMODULE_blog_export_msword_batch_process_finished($success$results$operations) {

  
// Load the data from Drupal's cache
  
$cache cache_get('famed:blog_export_results''cache');

  
// Unserialize the cache data
  
$blog_data unserialize($cache->data);
  
cache_clear_all('famed:blog_export_results''cache');
  if (
$blog_data) {
    
// Turn up error reporting
    
error_reporting (E_ALL|E_STRICT);
     
    
// Turn off WSDL caching
    
ini_set ('soap.wsdl_cache_enabled'0);
     
    
// Define credentials for LD
    
$credentials = array(
      
'username' => 'my_user_name',
      
'password' => 'my_password',
    );
     
    
// SOAP WSDL endpoint
    
$endpoint 'https://api.livedocx.com/1.2/mailmerge.asmx?WSDL';
     
    
// Define timezone
    
date_default_timezone_set('Europe/Berlin');

  

    
// Create a new instance of the SoapClient object
    
$soap = new SoapClient($endpoint);
    
$soap->LogIn(
      array(
        
'username' => $credentials['username'],
        
'password' => $credentials['password']
      )
    );
    
    
// Upload template
    
$path_to_template './'.drupal_get_path('module''MYMODULE').'/template.doc';
    
$data file_get_contents($path_to_template);
    if(empty(
$data)) {
      
drupal_set_message('Failed to read the template''error');
      
watchdog('famed''Failed to read the template'WATCHDOG_ERROR);
      return;
    }
    
    
$soap->SetLocalTemplate(array(
      
'template' => base64_encode($data),
      
'format'   => 'doc'
    
));
    
    
$fieldValues = array (
      
'author' => $blog_data['author']->name,
      
'email' => $blog_data['author']->mail,
      
'title'  => 'Blog Posts by '.$blog_data['author']->name,
    );
 

    
/**

     * In the template, these field  values are used on the title page of the document,

     * and in the header/footer of the doucment.

     */
    
$soap->SetFieldValues(array (
      
'fieldValues' => assocArrayToArrayOfArrayOfString($fieldValues)
    ));
    

    
// Block values is the repeating data, in this case, the contents of each blog post
    
$soap->SetBlockFieldValues(array(
      
'blockName' => 'blogpost',
      
'blockFieldValues' => multiAssocArrayToArrayOfArrayOfString($blog_data['block_values'])
    ));
    
    
// Build the document
    
$soap->CreateDocument();
    
    
// Get document as DOC
    
$result $soap->RetrieveDocument(array(
      
'format' => 'doc'
    
));

    
// Fetch the document
    
$data $result->RetrieveDocumentResult;
    
$filename './sites/default/files/blog.doc';
    if(
file_exists($filename)) {
      
unlink($filename);
    }

    
// Write the document to the filesystem
    
file_put_contents($filenamebase64_decode($data));

 

    
// Force the browser to download the document
    
if(file_exists($filename)) {
      
header ("Content-type: octet/stream");
      
header ("Content-disposition: attachment; filename=blog.doc;");
      
header("Content-Length: ".filesize($filename));
      
readfile($filename);
      exit;
    }
    else {
      
drupal_set_message('Failed to download the file''error');
    }
  }
  else {
    
// An error occurred.
    // $operations contains the operations that remained unprocessed.
    
$error_operation reset($operations);
    
$message t('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)));
  }
  
drupal_set_message($message);
 
}
?>
The data structures, which are sent to LiveDocx can be tricky to get right in PHP, so some additional functions are needed to massage the data that gets sent in the SetFieldValues() and SetBlockFieldValues() methods:
<?php

/**
 * Convert a PHP assoc array to a SOAP array of array of string
 *
 * @param array $assoc
 * @return array
 */
function assocArrayToArrayOfArrayOfString ($assoc) {
  
$arrayKeys   array_keys($assoc);
  
$arrayValues array_values($assoc);
  return array (
$arrayKeys$arrayValues);
}
 
/**
 * Convert a PHP multi-depth assoc array to a SOAP array of array of array of string
 *
 * @param array $multi
 * @return array
 */
function multiAssocArrayToArrayOfArrayOfString ($multi){
    
$arrayKeys   array_keys($multi[0]);
    
$arrayValues = array();
 
    foreach (
$multi as $v) {
      
$arrayValues[] = array_values($v);
    }
 
    
$_arrayKeys = array();
    
$_arrayKeys[0] = $arrayKeys;
 
    return 
array_merge($_arrayKeys$arrayValues);
}
?>
The trickiest part for me was getting the template correct in order for the LiveDocX service to work properly. I didn't know how to create merge blocks; as it turns out, it's as simple as inserting bookmarks into your template that follow a specific naming convention: blockstart_ blockend_ It's also important to know that LiveDocX is currently limited to having merge blocks defined in table cells. Future enhancements of the the service will support having merge blocks defined anywhere. I am excited for this to happen, as it will truly make this service a lot more flexible. The full API for LiveDocX can be found here.

Trackback URL for this post:

http://beyrent.net/trackback/269

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

getonlinegambling.com

getonlinegambling.com Our website is especially created for those students who need essay writing help, who want to get good grades without expensing their social life.

In real life,for most

In real life,for most people,having nice,clean and even cool car is their greatest dream,espeacially,car paiting in reality is available but people's viewpoints are always changing,well, Car Game on the computer can solve this program,it could allows us to do amazing things in your desirable way!

My 18months baby like bubble very much,when bathing,he always say bubble,bubble,i need bubble...so,i think most boys and girls like bubble trouble games ,they can keep them busy,well,for our adult,we can quietly go off attending to our daily chores,cooking,cleaning,washing,ironing...also I suggest playing My little pony Game,it is fun,too~!

Nowadays chindren are always looking for new friends because they don't like the feeling of alone,as an adult,maybe you give them different gift,or teach them how to play dress up Game for example,or even make friends with dora Game(like boots,too?) and winxgifts are the symbolic expressions for your inner emotions that your heart has preserved with utmost care,to a boy ,you can give him how to play magic Game,i believe they will like it !

you may encounter this situation,you are so eager to play or share something with your friend who want to shopping Game for example,but those friends you are planing to visit is out or busy,well,you can just surfing at the computer anytime,love girls dress up Game even Cars Game ? Ok,they will ready in an instant! want to have delicious food and don't know how to cook? food Game can help you to become a good chef! in a word,your no need waiting for any friend and schoolmates for school Game,they are available on the web!

Halloween is coming,i remember last year on Halloween my sister asked me to play baby Game with her,you know she loves baby very much,and i dressed up as a little prisoner,BOL! and my sister like barbie dress up Game because she wish to have a girl baby! also Halloween is a great time to throw a dog party ,there could be lots of dog Halloween costumes and patty games,we love play dog Game so much,meanwhile,some girls may like Cat Game,too,you can have a fun and safe pup party to celebrity Halloween!

hitsgambling.com

Excellent post. I want to thank you for this informative read, I really appreciate sharing this great post. Keep up your work. Thanks for this very useful info you have provided us. hitsgambling.com

http://www.pettopets.com

I am glad to found such useful post. I really increased my knowledge after read your post which will be beneficial for me. www.pettopets.com

Green Power International was

Green Power International was established in early 2002 in close cooperation with MWM GmbH (formerly Deutz Power System GmbH).
Landfill gas
Power generation

We are a customer oriented

We are a customer oriented organisation and we believe in putting our best foot forward in our journey to pinnacle.
Tulip projects
Vipul projects
BPTP upcoming projects in faridabad
3c projects in gurgaon

Gangaur Realtech is a

Gangaur Realtech is a professionally managed organisation specializing in real estate services where integrated services are provided by professionals to its clients seeking increased value by owning, occupying or investing in real estate.
Upcoming projects in gurgaon
Latest properties in gurgaon

Excelent

Now I know how to make an export tani hotel

good article

Great blog. All posts have something to learn. Your work is very good and i appreciate you and hopping for some more informative posts. tibia money | ffxi gil

Editorial- A presidential greeting

Paper Edition | Page: 6
On Thursday President Susilo Bambang Yudhoyono is scheduled to attend an annual Christmas celebration. His attendance (or failure) to attend what is usually a mundane ceremonial function, will be a landmark in the ongoing project of nationhood, apart from a test of his leadership.The Indonesian Ulema Council (MUI) renewed on Sunday an edict forbidding Muslims from expressing Christmas greetings. Its deputy, Ma鈥檙uf Amin, said this meant a Muslim "should not attend the ritual"; therefore Yudhoyono, a Muslim, should not attend the celebration as it would contain a Christian religious element.Alternately, conservatives suggest that lower level Christian officials should attend the national function in place of the President.But it is our position that Yudhoyono must attend the national Christmas celebration. Yudhoyono, elected directly by most voters twice in a row, needs to show a nation of more than 230 million people that all citizens are equal regardless of their background, in a country that has never been declared an Islamic nation.His presence would affirm the agreement of the founding fathers in 1945 that Indonesia would embrace citizens of diverse backgrounds, the roots of this being included in the 1928 Youth Congress that spoke of a pledge for one nation, despite its differences. However, Yudhoyono has shown signs of bowing to Muslim groups claiming a monopoly over Islamic interpretation, just as the likes of those currently controlling the MUI.A clear indication is his blatant absence to act in favor of the persecuted Ahmadiyah and Shia minorities {keyword} 鈥?many of them displaced and living in shelters. On Christmas Day, a few congregations held mass in front of the Presidential Palace 鈥?another clear sign of Yudhoyono鈥檚 refusal to intervene in the decisions of local administrations that have constrained congregations鈥?freedom to worship.It remains unclear where this nation is heading. Experts talk of a secular democracy that can accomodate religion formally, even though Islamic politicians have failed to insert the state鈥檚 recognition of sharia in the amended Constitution.Regardless of the solution, the religious freedoms of all citizens must be protected but currently {keyword} the opposite is the case.Yudhoyono could perhaps learn something from local authorities. In Jakarta, Deputy Governor Basuki "Ahok" Tjahaja Purnama opened his house to welcome well wishers. Jakarta Governor Joko Widodo visited the Jakarta Cathedral and Immanuel Church on Christmas Eve.And in Yogyakarta, interfaith groups including representatives of the Yogyakarta Palace visited Protestants and Catholics to wish them a merry Christmas at three churches in the area. Whether Muslims are sinning in expressing Christmas greetings to their brethren is a source of genuine anxiety for some of them.But many others, including this newspaper, are more worried about whether as {keyword} a nation we can sustain this long held mutual respect {keyword} and solidarity and therefore, whether we can sustain the nation itself if rigid barriers are set up among different groups.The attendance of our President at the national Christmas gathering would help to affirm, if only in a ceremonial manner, the foundation of this nation, that Indonesia is truly the home of all Indonesians, regardless of their faith.

Woolrich Arctic Parka

was moved from the 73,000 seat open air Bank of of Woolrich Arctic Parka of America Stadium to the smaller TimeWarner Cable Arena, which
hold about 21,000. The move was made because of a a Woolrich Arctic Parka a forecast of rain and thunderstorms."I've been looking forward to
for a really long time," Madeline Frank of Charlotte, N.C., N.C., Woolrich Parka N.C., said today. "I am just feeling really let down
like bummed. It was kind of my dream to see see Woolrich Jackets see him speak, so definitely really sad."Watch our ABC News/Yahoo
Convention Show LIVE tonight, 7-11:30 PM HEREAlthough she isn't old old Woolrich Jackets old enough to vote, Madeline knocked on doors and canvassed
neighborhood for nine hours so she could see the president president Woolrich Arctic Parka Women president when he came to her hometown Thursday because she

http://www.longchampsingaporesales.net/

Als junger Designer, hat Joseph Altuzarra nicht den Luxus der Ann?herung Resort als Verschnaufpause zwischen den Modenschauen. Dies ist nicht zu vermuten, dass der Rest der Modewelt wird aufwirbeln ihre http://www.longchampsingaporesales.net/ Fersen und telefonieren in Sammlungen. Die Herstellung dieses viele Kleider in einem halsbrecherischen Tempo ist nicht 6 Wochen auf den Seychellen, egal wie viele Male Sie haben es geschafft. Aber w?hrend longchamp sale diese etablierten Namen haben, gut, ihre Vision gegründet, kann Resort eine Zeit für sie leichter zug?nglich Versionen von Dingen, die sie im Frühjahr oder Herbst eingeführt zu schaffen sein. longchamp outlet

aussieBum underwear

Now in a few places one's country, calvin klein underwear any number of people are already cutting straight down the particular flowers inside the forests aussieBum Retro Swimwear since they need wood plus more farmland.aussieBum Surf Shorts The particular areas regarding forests are usually getting smaller and also smaller.Several scientists point out in which you will have simply no great woods inside 20 or perhaps 25 decades.Womens Calvin klein It is any terrible factor.aussieBum Underwear The location where the woods fade, airborne dirt and dust storms can take place occasionally.The particular weather are certain to get very hot and also dried up.The particular whole earth can be a huge desert..

Celine Handbags

All celine bag, is a must-have travelling bag if anyone is many of the twelve months about! Changing seasons arrive plus changing seasons move, Celine Boogie Bag continues to a must-have travelling bag those who will be interested popular.Most people definitely like a Celine Box Bag.Its quintessentially amazing plus elegant.Celine essentials bag coffe is definitely designed during distinctive design and style.The leading section which includes a diddly wallet including a tassel.The following design and style travelling bag appears a person's travelling bag cheerful back, which assist a person's day-to-day joyful.Celine Phantom Bag appears a person's travelling bag smiling back, which assist a person's on a daily basis chuffed.Celine Purses often be inspired by hottest design, hese awesome women of all ages set storage compartments will be awesome hove taken care of performance either on the job plus all over a person's relaxing adventures celine designer purse.2010, the usual explosion devices Celine release with style and design simplicity, setting off a luxury Celine Tote Bag Popular, introduced a number of sophisticated plus portable Tote Bag.

module

sounds very complicated to me.
i'm wondering if somewhere out there have a module for download a content as word or pdf format.
if you know something please let me know.

thanks mate.

Hi, Nice post. I want to

Hi,

Nice post. I want to export CV's (using the resume module) into word. Can the above be relevant for the task at hand?
Thanks.
Tafa

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

About Erich

Erich is a web developer and a native New Englander who is passionate about life, the universe, and everything.

He is a Drupal consultant, previously employed as a senior developer at Harvard University, working on the IQSS OpenScholar project.  Prior to joining the team at Harvard, he was the engineering manager at CommonPlaces e-Solutions, in Hampstead, NH, contributing as the lead engineer on the Greenopolis.com and Twolia.com.

Erich is active in the Drupal community, having contributed modules and patches to the community. He presented at DrupalCon in Szeged Hungary, and co-presented at DrupalCon 2009 in Washington, DC.

Erich lives in New Hampshire with his wife, two sons, and three weimaraners.  When not writing code, Erich enjoys landscaping and woodworking.

Faceted search

Categories

Content type

Project types

Artwork Type

Artwork Tags

Recent comments

Activity Stream

August 29, 2011

August 25, 2011

August 24, 2011

August 23, 2011

August 15, 2011

August 11, 2011

August 10, 2011

August 9, 2011

August 4, 2011

August 3, 2011