The Cheeky Monkey Media Blog

A few words from the apes, monkeys, and various primates that make up the Cheeky Monkey Super Squad.

Building a Custom Module banner

This is part 3, of a 3 part tutorial for creating a custom module for drupal 7. If you haven’t already done so, you may wish to look at part 1 and part 2 first. Anyway, here is the source code of this tutorial.

First of all, though, I have to apologize that I did not do a good job of keeping track of all of the changes made so far. I code everything before I start writing, so hopefully it is not too confusing.

There are few things I would like update this time so here is a list of the things that I want to cover in this tutorial:

  • Update the module menu, so each recipe operation has their unique path.
  • Implement helper functions to handle add/edit/update operations
  • Implement functions to handle recipe status changes via ajax call.
  • Implement functions to handle delete recipes.

Update the module menu, so each recipe operation has their unique path.

Time to update the hook_menu to clear up the mess that I left from my last tutorial. Let’s use a constant for the recipe path, so it’s easier to update it. Add the following line to the top of the simple_recipe.module

define ('SIMPLE_RECIPE_BASE_PATH', ‘simple_recipe');

Then let’s make a menu item to list all recipes, so the end user could actually use this thing. Let’s add the following menu items to the hook_menu array.

$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH . '/list'] = array(
'title' => 'Listing all Recipes',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => 0,
);

You might wonder why there is no callback for this path. If we declare menu item type to be MENU_DEFAULT_LOCAL_TASK we have to put the actual callback in other items, strange isn’t it? Anyways, let’s declare another menu item. Please add the following code to your hook_menu:

$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH] = array(
'title' => 'Simple Recipe',
'description' => 'List recipes',
'page callback' => '_simple_recipe_list_recipes',
'page arguments' => array(1),
'access callback' => '_simple_recipe_access_check',
'access arguments' => array(1),
'type' => MENU_LOCAL_TASK,
);

This MENU_LOCAL_TASK has to do all the hard work.

Then, we need to update the path for add and edit operation.

$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH . '/add'] = array(
'title' => 'Add a Simple Recipe',
'description' => 'Add recipe',
'page callback' => 'drupal_get_form',
'page arguments' => array('simple_recipe_form'),
'access callback' => '_simple_recipe_access_check',
'access arguments' => array(1),
'type' => MENU_CALLBACK,
);
$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH . '/edit/%'] = array(
'title' => 'Edit Simple Recipe',
'description' => 'Edit recipe',
'page callback' => 'drupal_get_form',
'page arguments' => array('simple_recipe_form'),
'access callback' => '_simple_recipe_access_check',
'access arguments' => array(1),
'type' => MENU_CALLBACK,
);

As you can see, both operations use the same simple_recipe_form form. I need to make some changes to make it work. Basically add a hidden field to store recipe id to distinguish the insert and update operation. Let’s focus on updating this hook_menu for now. Register the last two operations, delete and status changes.

$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH . '/delete'] = array(
'title' => 'Delete Recipe',
'description' => 'Delete recipe',
'page callback' => 'drupal_get_form',
'page arguments' => array('simple_recipe_delete_form'),
'access callback' => '_simple_recipe_access_check',
'access arguments' => array(1),
'type' => MENU_CALLBACK,
);

Since delete operation is quite destructive, we want to make confirmation form for this operation. The status change operation will be done via ajax, so we don’t need to create a form for that. The placeholder %user tells drupal to load a user object as a first argument, and the last % is a placeholder for recipe id.

$items['user/%user/' . SIMPLE_RECIPE_BASE_PATH . '/status/%'] = array(
'title' => 'Update a Simple Recipe status',
'description' => 'Update recipe status',
'page callback' => 'simple_recipe_update_status',
'page arguments' => array(4),
'access callback' => '_simple_recipe_access_check',
'access arguments' => array(1),
'type' => MENU_CALLBACK,
);

Implement helper functions to handle add/edit/update operations

Now we have all the menu item path re-arranged, we are going to need few functions to help us create, load and update recipes. The old simple recipe save function is not very flexible. It only allows us to save a new recipe and use a hardcoded value for status. Let’s change that. Basically, I want to insert and update to be one function, that’s why I am using db_merge function here. This function can be view as a combination of db_insert and db_update, so when rid (recipe id) present in the key() function, it will do an update. Otherwise, it will do an insert operation

/**
* Update simple recipe db, insert or update.
* @param array $values
*/
function simple_recipe_save_recipe($values = null) {
global $user;
$query = db_merge('simple_recipe')
// Check if rid exist or not, if found do update otherwise do insert.
->key(array('rid' => isset($values['rid']) ? $values['rid'] : 0))
->fields(array(
'uid' => isset($values['uid']) ? $values['uid'] : $user->uid,
// Make sure serialize form_value before passing to this function.
'form_values' => isset($values['form_values']) ? $values['form_values'] : '',
'status' => (int) $values['status'],
'timestamp' => REQUEST_TIME,
));
$query->execute();
}

Next, we are going to add delete function. This is really straight forward.

/**
* Delete a database record from db.
* @param int $rid
*/
function simple_recipe_delete($rid) {
if (is_numeric($rid)) {
$num_deleted = db_delete('simple_recipe')
->condition('rid', $rid)
->execute();
}
}

There are two functions to get recipes, one load a recipe for a given the recipe id. The other loads recipes for a given user and recipe status.

/**
* load a recipe from db.
* @param int $noid
* @return db record.
*/
function simple_recipe_get_recipe($rid) {
return db_select('simple_recipe', 'sr')
->fields('sr')
->where('rid = :rid', array(
':rid' => (int)$rid,
))
->execute()
->fetchAssoc();
}
/**
* Load recipes from a given user.
*
* @param int $uid
* User id.
* @param int $status
* recipe status, 0 disabled, 1 enabled, other load everything.
* @return array of recipes objects
*/
function simple_recipe_get_recipes($uid, $status = 2) {
$query = db_select('simple_recipe', 'sr')
->fields('sr')
->condition('sr.uid', $uid);
// Decide the what to load.
switch($status) {
case 0:
// Disabled recipts.
$query->condition('sr.status', 0);
break;
case 1:
// Enabled recipts.
$query->condition('sr.status', 1);
break;
default:
// Load every recipts.
break;
}
$result = $query->execute()
->fetchAll();
return $result;
}

Now, we are ready to update operations related to recipes.

Implement functions to handle recipe status changes via ajax call.

Here is the todo list:

  1. register a path for recipe status callback.
  2. create a callback function to change recipe’s status
  3. use drupal default ajax library to ajaxify the status update link
  4. update the recipe status operation label by using javascript

Step 1: register a path for recipe status callback.

We have registered the path user/%user/simple_recipe/status/% in the hook_menu already.

Step 2: create a callback function to change recipe’s status.

Let’s implement the simple_recipe_update_status callback function. We load a existing recipe from database and flip the status value then save it.

/**
* Flip status flag for the given simple recipe id.
* @param int $rid
*/
function simple_recipe_update_status($rid) {
$recipe = simple_recipe_get_recipe($rid);
$recipe['status'] = (int) !$recipe['status'];
simple_recipe_save_recipe($recipe);
}

Step 3: use drupal default ajax library to ajaxify the status update link.

Let’s create the link in the recipe listing page. Since the listing page is not a form. We can’t piggy-back on the form api to perform a ajax call. Therefore, we are going to use the core ajax library to ajaxify this link. Its very simple, we need to do two things. load the library and then update the link class. Modify _simple_recipe_list_recipes function, let’s add the following line to the top of the function.

drupal_add_library('system', 'drupal.ajax');

Then we need to add use-ajax class to the $operation variable which holds text either Enable or Disable.

l(t($operation), "user/$account->uid/simple_recipe/status/$recipe->rid", array(
'attributes' => array(
'title' => 'Title here.',
'id' => $status_id,
'class' => array('use-ajax'), // Ajaxify this link.
),
)),

Now, it’s time to update the link text after each successful ajax call. We want to display label Enable when the recipe with status is 0 and Disable for status 1. We are going to do this using javascript. Add this line right after the for each loop.

drupal_add_js(drupal_get_path('module', 'simple_recipe') . '/js/simple_recipe.js');

Step 4: update the recipe status operation label by using javascript.

Make sure you create a new directory call js inside our simple_recipe directory, then create a new file call simple_recipe.js. In order to update each recipe’s status, we need to be able to detect which recipe’s status link were clicked. In the Jquery ajaxSuccess function, we have a setting.url property, which stores the request url. We know the last segment of the path user/%user/simple_recipe/status/% holds the recipe id, so by extracting the id from the request URI we will be able to target the right element. add the following code to simple_recipe.js file.

(function ($) {
Drupal.behaviors.simple_recipe = {
attach: function (context, settings) {
// listen to ajax success event.
// Then we are going to use url to determine which
// recipe has been fired via ajax.
// Once we get the id, then we can alter the recipe state text.
$(document).ajaxSuccess(function(event, xhr, settings) {
// Recipe id is after the string status/
var pattern = /status/(d+)/;
var str = settings.url;
var result = str.match(pattern);
// Only update recipe status if we have a valid id.
if (result[1].length > 0) {
var id = result[1];
var id_selector = '#status-' + id;
// Update text.
if ($(id_selector).text() == 'Disable') {
$(id_selector).text('Enable');
}
else {
$(id_selector).text('Disable');
}
}
});
}
}
})(jQuery);

Implement functions to handle delete recipes.

Let’s create a new form to display in the formation page. This form will have a hidden field that stores the recipe id.

function simple_recipe_delete_form($rid) {
$rid = arg(4);
$recipe = simple_recipe_get_recipe($rid);
$recipe = unserialize($recipe['form_values']);
$form['warning'] = array(
'#markup' => '

‘ . t(‘Are you sure you want to delete @name?’, array(‘@name’ => $recipe[‘name’])) . ‘

‘, ); $form[‘rid’] = array( ‘#type’ => ‘hidden’, ‘#value’ => $rid, ); $form[‘delte’] = array( ‘#type’ => ‘submit’, ‘#name’ => ‘delete_btn’, ‘#value’ => ‘Delete’, ); return $form; }

Once the form is submitted, we will check for the hidden recipe id and delete it from the database. I am using very similar approach to distinguish the insert and update operation on the simple_recipe_form form. Note that instead of using drupal_goto we are using form_state to do the redirect, that’s because we want to give the; other module a chance to process form as well.

function simple_recipe_delete_form_submit($form, &$form_state) {
if (!empty($form_state['values']['rid']) && !empty($form_state['triggering_element']['#name']) && $form_state['triggering_element']['#name'] == 'delete_btn') {
$rid = $form_state['values']['rid'];
simple_recipe_delete($rid);
global $user;
$form_state['redirect'] = 'user/' . $user->uid . '/' . SIMPLE_RECIPE_BASE_PATH;
drupal_set_message(t('Delete successful.'));
}
}

Find the source code here:

Download Source Code

Need help with a Drupal project? Did you know that Cheeky Monkey Media works with other agencies that need extra hands on projects? Our Drupal Web Developers are ready to help. Call us!