Part 2 - How to display Drupal 7 nodes on a html page using jquery
In this post, I will try to explain as much as I can on how to extend resources for Drupal 7 services module to accomplish the remaining of this little project. I think I have to give you a disclaimer before I continue on this task. This is my first attempt to write a druapl module (a hook for Drupal 7 services module), and because of my lack of understanding the overall Drupal 7 architecture, this post may contain inaccurate information. But, don't get too discouraged in following my example. As you can see later, I was able to accomplish what I needed to do with my little knowledge of Drupal 7 module development. In fact, I have to give a credit to this post, "An introduction to Drupal 7 RESTful Services" by Alex Rayu. I strongly encourage to read it. Also, I found this tutorial extremely useful: https://drupal.org/node/1074362.
Before writing the module, let me explain why I am building the custom module instead of using default node resource provided by services 3.x module. If you invoke "retrieve" method of the default node resource, you will get something similar to the list below.
Here is the list of issues I have to overcome:
I think I have enough problems to justify my decision to implement a custom resource. All right! Let's get started.
1. Create a folder called "my_webservices_resources" in Drupal 7 custom module folder. In my case, the path is /var/www/sites/all/modules/custom.
2. Create a file called "my_webservices_resources.info" in a newly created folder and type the following content:
name = My Web Service Resources
description = Custom web service resources for my drupal 7 site (e.g. News and Annoucements)
core = 7.x
version = 7.x-0.1-dev
dependencies[] = services
Each entry is self-descriptive, but let me explain a little bit for users (like me) who have no experience with Drupal 7 module installation and/or construction. The name and description can be anything you like, but you have to specify 7.x in core which indicates your are making a module that is compatible with Drupal 7 core. For version, you must start with 7.x- and add services in dependencies to indicate that your module needs services module in order to function.
4. Create another file called "my_webservices_resources.module" in the same folder and type the following empty functions.
/**
* Implements hook_services_resources().
*/
function my_webservice_resources_services_resources() {
// TO DO
}
/**
* Callback function for custom endpoint
*/
function _my_customnode_retrieve($fn, $type, $tid, $order, $num, $wc) {
// TO DO
}
/**
* Gets nodes
**/
function my_customnode_items($type, $tid, $order, $num, $wc) {
// TO DO
}
Basically, I am only using three functions to retrieve nodes (news_announcement content type). The first one, my_webservice_resources_service_resources(), is the most important function since it is a hook to the services module. You need to define your resource in this hook in order to be visible in Services module configuration panel. Let's create one.
5. Complete my_webservice_resources_service_resources() function as shown below.
function my_webservice_resources_services_resources() {
$api = array(
'customnode' => array(
'retrieve' => array(
'help' => 'Retrieve news_announcement type nodes',
'callback' => '_my_customnode_retrieve',
'access callback' => 'user_access',
'access arguments' => array('access content'),
'access arguments append' => FALSE,
'args' => array(
array(
'name' => 'fn',
'type' => 'string',
'description' => 'Function to perform',
'source' => array('path' => '0'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'type',
'type' => 'string',
'description' => 'Content Type',
'source' => array('param' => 'type'),
'optional' => TRUE,
'default' => 'news_announcements',
),
array(
'name' => 'tid',
'type' => 'int',
'description' => 'Content Type',
'source' => array('param' => 'tid'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'order',
'type' => 'string',
'description' => 'Sorting Order',
'source' => array('param' => 'order'),
'optional' => TRUE,
'default' => 'DESC',
),
array(
'name' => 'num',
'type' => 'int',
'description' => 'Number of records returned',
'source' => array('param' => 'num'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'wc',
'type' => 'int',
'description' => 'Word counts for teaser field',
'source' => array('param' => 'wc'),
'optional' => TRUE,
'default' => '0',
),
),
),
),
);
return $api;
}
Note that the callback function is specified in this $api array. This function will be called when "retrieve" method is invoked. Let's implement the callback function, _my_customnode_retrieve().
6. Complete the callback function as shown below.
/**
* Callback function for custom endpoint
*/
function _my_customnode_retrieve($fn, $type, $tid, $order, $num, $wc) {
$tid = intval($tid);
$num = intval($num);
$wc = intval($wc);
// default value
$type = 'news_announcements';
if(empty($order)) $order = 'DESC';
return my_customnode_items($type, $tid, $order, $num, $wc);
}
In this function, I am sanitizing parameters before calling the actual module that creates a dynamic query and executes the query to retrieve desirable result. Note that I am forcing $type to 'news_announcements'. I don't think I even need this piece, but I will leave it for future improvement. Finally, let's put the final piece of this simple puzzle, my_customnode_items() function.
7. Complete the function as shown below.
/**
* Gets nodes
**/
function my_customnode_items($type, $tid, $order, $num, $wc) {
// create query
$query = db_select('node', 'n');
$query->join('node_revision', 'v', '(n.nid = v.nid) AND (n.vid = v.vid)');
$query->join('users', 'u', 'n.uid = u.uid');
$query->join('field_data_body', 'b', '((b.entity_type = \'node\') AND (b.entity_id = n.nid) AND (b.revision_id = n.vid))');
$query->join('field_data_field_announcement_type', 'a', '((a.entity_type = \'node\') AND (a.entity_id = n.nid) AND (a.revision_id = n.vid))');
$query->fields('n', array('nid','created', 'changed'));
$query->fields('v', array('timestamp', 'title'));
$query->addField('u', 'name', 'author');
$query->addField('b', 'body_value', 'teaser');
$query->addField('a', 'field_announcement_type_tid', 'tid');
$query->addField('n', 'title', 'path'); // this field will be replace with path ***
$query->condition('n.type', $type, '=');
if($tid != 0) $query->condition('a.field_announcement_type_tid', $tid, '=');
// order of result set
$query->orderBy('n.created', $order);
// number of records returned
if($num != 0) $query->range(0, $num);
$items = $query->execute()->fetchAll();
foreach($items as $record) {
$record->teaser = strip_tags(trim(str_replace(' ', '', $record->teaser)));
if($wc != 0) $record->teaser = string_excerpt($record->teaser, $wc);
$record->path = '';
$record->path = $GLOBALS['base_url'].$GLOBALS['base_path'].drupal_lookup_path('alias', "node/".$record->nid);
}
return $items;
}
I almost forgot to include one utility function to tokenize (break it in words) the body. I got this code from somewhere, but I cannot remember when and where I got it from. Sorry. The credit goes to whoever created this function.
function string_excerpt($string, $count) {
$words = explode(' ', $string);
if(count($words) > $count)
{
$words = array_slice($words, 0, $count);
$string = implode(' ', $words);
}
return $string;
}
8. Go to Drupal 7 Modules management and enable a newly created module (My Web Service Resources) and then, go to Services configuration setting.
9. Enable your custom resource as shown below.
10. In order to access a custom resource via jsonp, you need to type something like this: http://yourdomain/yourendpoint/myresource/retrieve.jsonp?num=3&tid=43&wc=10
Note that tid is the taxonomy term id to indicate a specify category of "news_announcements" content type. I guess the person (guru) who has implemented Drupal CMS in my workplace need to sub-categorize "News and Announcements" type. The parameter num indicates the number of records and wc indicates the word count for teaser. By the way, in this example, I am using a custom content type called "announcements". Simply replace this with your custom content type (see Step 7).
Here is the result. I hope this helps.
Before writing the module, let me explain why I am building the custom module instead of using default node resource provided by services 3.x module. If you invoke "retrieve" method of the default node resource, you will get something similar to the list below.
Here is the list of issues I have to overcome:
- There is no way to control the number of records (items) to be returned by the built-in node resource. By default, it returns all records. Although you can target a specific content type by specifying the parameter such as ?parameters[type]=news_announcements, it still pose a problem if the specific content type contains a large amount of nodes.
- The item does not contain the body (content) of the node, nor its path to the html view. The uri only points to a node of the web service end point (e.g., rest).
- There is no way to sort items in specific order (descending or ascending).
- To access "content body" and "path" of the node, the client (consumer page) must make another call to the path specified in uri.
I think I have enough problems to justify my decision to implement a custom resource. All right! Let's get started.
1. Create a folder called "my_webservices_resources" in Drupal 7 custom module folder. In my case, the path is /var/www/sites/all/modules/custom.
2. Create a file called "my_webservices_resources.info" in a newly created folder and type the following content:
name = My Web Service Resources
description = Custom web service resources for my drupal 7 site (e.g. News and Annoucements)
core = 7.x
version = 7.x-0.1-dev
dependencies[] = services
4. Create another file called "my_webservices_resources.module" in the same folder and type the following empty functions.
/**
* Implements hook_services_resources().
*/
function my_webservice_resources_services_resources() {
// TO DO
}
/**
* Callback function for custom endpoint
*/
function _my_customnode_retrieve($fn, $type, $tid, $order, $num, $wc) {
// TO DO
}
/**
* Gets nodes
**/
function my_customnode_items($type, $tid, $order, $num, $wc) {
// TO DO
}
Basically, I am only using three functions to retrieve nodes (news_announcement content type). The first one, my_webservice_resources_service_resources(), is the most important function since it is a hook to the services module. You need to define your resource in this hook in order to be visible in Services module configuration panel. Let's create one.
5. Complete my_webservice_resources_service_resources() function as shown below.
function my_webservice_resources_services_resources() {
$api = array(
'customnode' => array(
'retrieve' => array(
'help' => 'Retrieve news_announcement type nodes',
'callback' => '_my_customnode_retrieve',
'access callback' => 'user_access',
'access arguments' => array('access content'),
'access arguments append' => FALSE,
'args' => array(
array(
'name' => 'fn',
'type' => 'string',
'description' => 'Function to perform',
'source' => array('path' => '0'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'type',
'type' => 'string',
'description' => 'Content Type',
'source' => array('param' => 'type'),
'optional' => TRUE,
'default' => 'news_announcements',
),
array(
'name' => 'tid',
'type' => 'int',
'description' => 'Content Type',
'source' => array('param' => 'tid'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'order',
'type' => 'string',
'description' => 'Sorting Order',
'source' => array('param' => 'order'),
'optional' => TRUE,
'default' => 'DESC',
),
array(
'name' => 'num',
'type' => 'int',
'description' => 'Number of records returned',
'source' => array('param' => 'num'),
'optional' => TRUE,
'default' => '0',
),
array(
'name' => 'wc',
'type' => 'int',
'description' => 'Word counts for teaser field',
'source' => array('param' => 'wc'),
'optional' => TRUE,
'default' => '0',
),
),
),
),
);
return $api;
}
Note that the callback function is specified in this $api array. This function will be called when "retrieve" method is invoked. Let's implement the callback function, _my_customnode_retrieve().
6. Complete the callback function as shown below.
/**
* Callback function for custom endpoint
*/
function _my_customnode_retrieve($fn, $type, $tid, $order, $num, $wc) {
$tid = intval($tid);
$num = intval($num);
$wc = intval($wc);
// default value
$type = 'news_announcements';
if(empty($order)) $order = 'DESC';
return my_customnode_items($type, $tid, $order, $num, $wc);
}
In this function, I am sanitizing parameters before calling the actual module that creates a dynamic query and executes the query to retrieve desirable result. Note that I am forcing $type to 'news_announcements'. I don't think I even need this piece, but I will leave it for future improvement. Finally, let's put the final piece of this simple puzzle, my_customnode_items() function.
7. Complete the function as shown below.
/**
* Gets nodes
**/
function my_customnode_items($type, $tid, $order, $num, $wc) {
// create query
$query = db_select('node', 'n');
$query->join('node_revision', 'v', '(n.nid = v.nid) AND (n.vid = v.vid)');
$query->join('users', 'u', 'n.uid = u.uid');
$query->join('field_data_body', 'b', '((b.entity_type = \'node\') AND (b.entity_id = n.nid) AND (b.revision_id = n.vid))');
$query->join('field_data_field_announcement_type', 'a', '((a.entity_type = \'node\') AND (a.entity_id = n.nid) AND (a.revision_id = n.vid))');
$query->fields('n', array('nid','created', 'changed'));
$query->fields('v', array('timestamp', 'title'));
$query->addField('u', 'name', 'author');
$query->addField('b', 'body_value', 'teaser');
$query->addField('a', 'field_announcement_type_tid', 'tid');
$query->addField('n', 'title', 'path'); // this field will be replace with path ***
$query->condition('n.type', $type, '=');
if($tid != 0) $query->condition('a.field_announcement_type_tid', $tid, '=');
// order of result set
$query->orderBy('n.created', $order);
// number of records returned
if($num != 0) $query->range(0, $num);
$items = $query->execute()->fetchAll();
foreach($items as $record) {
$record->teaser = strip_tags(trim(str_replace(' ', '', $record->teaser)));
if($wc != 0) $record->teaser = string_excerpt($record->teaser, $wc);
$record->path = '';
$record->path = $GLOBALS['base_url'].$GLOBALS['base_path'].drupal_lookup_path('alias', "node/".$record->nid);
}
return $items;
}
I almost forgot to include one utility function to tokenize (break it in words) the body. I got this code from somewhere, but I cannot remember when and where I got it from. Sorry. The credit goes to whoever created this function.
function string_excerpt($string, $count) {
$words = explode(' ', $string);
if(count($words) > $count)
{
$words = array_slice($words, 0, $count);
$string = implode(' ', $words);
}
return $string;
}
8. Go to Drupal 7 Modules management and enable a newly created module (My Web Service Resources) and then, go to Services configuration setting.
9. Enable your custom resource as shown below.
10. In order to access a custom resource via jsonp, you need to type something like this: http://yourdomain/yourendpoint/myresource/retrieve.jsonp?num=3&tid=43&wc=10
Note that tid is the taxonomy term id to indicate a specify category of "news_announcements" content type. I guess the person (guru) who has implemented Drupal CMS in my workplace need to sub-categorize "News and Announcements" type. The parameter num indicates the number of records and wc indicates the word count for teaser. By the way, in this example, I am using a custom content type called "announcements". Simply replace this with your custom content type (see Step 7).
Here is the result. I hope this helps.
Amazing. Thats quite a query.
ReplyDelete