A Drupal 6 project I'm working on requires me to discourage duplicate titles for a node type. I want to have a Vocabulary Item node type, where the title is the word itself. I don't want to prohibit duplicate titles, but just discourage creating a new Vocabulary Word if one already exists with the same title.
For example, if an entry exists for "turkey" (the bird) I want to inform the user that there is already an entry for "turkey". But I don't want to prohibit them from creating a new Vocabulary Item called "Turkey" (the country).
So how to go about doing this? It turns out it's really not too hard, once you know your way around the Drupal 6 API. First, let's look at the result, and then I'll show you the code that makes it go.
Instead of just having the standard node add form, were going to make creating a Vocabulary Item node a potentially 3 step process:
To begin the first step, the user will select a link to be taken to the "New Vocabulary Item" title entry page, and be prompted to enter a title for the new item:

After selecting "Continue", the word is checked against existing Vocabulary Item node titles. (Please forgive me for using "Turkey", which is not a Spanish word, as it's not important to the discussion!) If there are no duplicates, the user is taken directly to the node add form. If there are existing Vocabulary Items with the same title, the user will be notified of the duplicates, and asked to confirm that they would actually like to create a new node:

If the user clicks "Cancel", they will be taken back to the title collection form, and if they click "Continue" they will be taken to the node form to add a new Vocabulary Item node:

There are two major differences here with the standard node form. Rather than the standard "Create Vocabulary Item" title, the title is "Create "Turkey"", and the text field for the node title is missing. The user is now creating a node titled "Turkey", and the ability to alter the title has been removed. If this node is saved, and then opened again for editing using the "edit" tab, the title field will still be missing. The title has effectively been frozen as "Turkey".
OK, so now that we know what the result looks like, let's take a look at my approach to doing this. I've got a custom module called le_custom that does all the work.
The first thing to do is create the pages for the first two steps above, the title collection page, and the duplicate titles warning page. I create a few menu items using hook_menu(), the first of which will be added to a menu somewhere such as "Create Content":
le_custom.add-vocab.page.inc is where the pages are defined. Here is what the title collection page to collect the Vocabulary Item title looks like:
It's a very simple form. All the interesting stuff happens in the submit function. I've used a Views 2 view to check for duplicate titles, as well as generate an HTML table of nodes with the same title if they exist. I'll take a look at that view in a moment. Basically, I programatically execute that view, and then fork on the result. If no duplicate titles are found, I stash away the title in a session variable, and redirect to the node form. (Note: In Drupal, added variables in sessions must be arrays, so setting $_SESSION['le_custom_new_word'] will not store the variable in the session) If duplicates are found, I set an warning message, and redirect to the warning page that will alert the user of the duplicates. The code for the warning page looks like this:
I'm using the same view as I did on the title entry page. Note that there is an argument on the URL for the warning page, so it's possible for users to skip the title entry page by going directly to add-vocab/Turkey. That's fine. If no duplicate titles are found, the title that is supplied on the URL is stashed away the session variable, and the user is simply redirected to the node add form, just like I did when in the title collection submit function.
On the duplicates warning page, I don't just check to see if there are duplicates, I actually display them. Fortunately, Views has done the work of generating an HTML table for me. Lets take a look at that view:

It's a very basic View. It filters nodes to those of the Vocabulary Item type, and takes a title as an argument, which further filters those nodes down to just those with the title specified in the argument. Two fields are output: the title, and the short translation for the Vocabulary Item (e.g. a large tasty bird). The style is "table", so the output is formatted as an HTML table.
On the warning page's submit function, the page redirection will depend on which button the user clicked. Cancel will go back to the title entry page, and continue will, as we've seen twice before now, stash away the title in a session variable, and redirect the the node add form.
So now we see how the first two pages work. A title is collected, the user is warned if there are already Vocabulary Item nodes with the given title, and then taken to the node add form with the title stashed away in the session.
So, let's take a look at the code that tweaks the node add form to our suit my purpose. I use hook_form_alter() like this:
Up at the top, you'll notice there is a check on a permission. I've added in my le_custom module a permission called 'edit vocab titles' that allows a user to skip the title entry page, go directly to the node add form to create a Vocabulary Item, and have access to the node title field just like on the standard node form:
That is useful for users with some kind of 'editor' role. For users without the permission, I've hidden the title field ($form['title']['#type'] = 'hidden';) and added a new validation function to the form that enforces the prohibition against changing the title:
This is not strictly necessary, since the user will not be able to change the title on the form anyway, but I'm doing this just to be thorough.
If the user is just editing the form, there are no other changes that need to be made. The standard node form title (which is the title of the node), is fine. If this is a new node, we need to grab the title from the session variable, and set the form's title field with it, as well as change the title of the page from the standard "Create Vocab Item", so that the user knows the title of the Vocabulary Item node they are creating.
If the user tries to come to the node add page, and the session variable for the title is not set, we'll send them to the title collection page:
And that really about it! Just one more small detail:
I've implemented hook_init() so that the session variable that stashes away the title is cleared on every page load, unless the page being loaded is the node add form for a Vocabulary Item. This avoids leaving that session variable lying around if it's not going to be used, as in the event that the user starts entering data on the node form, and then navigates away for some reason.
It's probably possible to build this up into a configurable contributed module, where the administrator specifies the node types for which they would like to restrict duplicate titles, and provides a view that does the work of checking and listing duplicates. I will leave that as an exercise for the reader.
For example, if an entry exists for "turkey" (the bird) I want to inform the user that there is already an entry for "turkey". But I don't want to prohibit them from creating a new Vocabulary Item called "Turkey" (the country).
So how to go about doing this? It turns out it's really not too hard, once you know your way around the Drupal 6 API. First, let's look at the result, and then I'll show you the code that makes it go.
Instead of just having the standard node add form, were going to make creating a Vocabulary Item node a potentially 3 step process:
To begin the first step, the user will select a link to be taken to the "New Vocabulary Item" title entry page, and be prompted to enter a title for the new item:

After selecting "Continue", the word is checked against existing Vocabulary Item node titles. (Please forgive me for using "Turkey", which is not a Spanish word, as it's not important to the discussion!) If there are no duplicates, the user is taken directly to the node add form. If there are existing Vocabulary Items with the same title, the user will be notified of the duplicates, and asked to confirm that they would actually like to create a new node:

If the user clicks "Cancel", they will be taken back to the title collection form, and if they click "Continue" they will be taken to the node form to add a new Vocabulary Item node:

There are two major differences here with the standard node form. Rather than the standard "Create Vocabulary Item" title, the title is "Create "Turkey"", and the text field for the node title is missing. The user is now creating a node titled "Turkey", and the ability to alter the title has been removed. If this node is saved, and then opened again for editing using the "edit" tab, the title field will still be missing. The title has effectively been frozen as "Turkey".
OK, so now that we know what the result looks like, let's take a look at my approach to doing this. I've got a custom module called le_custom that does all the work.
The first thing to do is create the pages for the first two steps above, the title collection page, and the duplicate titles warning page. I create a few menu items using hook_menu(), the first of which will be added to a menu somewhere such as "Create Content":
<?php/**
* Implements hook_menu
*/function le_custom_menu() {
$items = array();
$items['add-vocab'] = array(
'title' => 'New Vocabulary Item',
'page callback' => 'le_custom_new_vocabitem',
'access arguments' => array('create vocab content'),
'file' => 'le_custom.add-vocab.page.inc',
'type' => MENU_NORMAL_ITEM,
);
$items['add-vocab/%'] = array(
'page callback' => 'le_custom_confirm_new_vocabitem',
'page arguments' => array(1),
'access arguments' => array('create vocab content'),
'file' => 'le_custom.add-vocab.page.inc',
'type' => MENU_CALLBACK,
);
return $items;
}?><?php/*
* Page to start the process of creating a new vocabulary item
*/function le_custom_new_vocabitem() {
$output = drupal_get_form('le_custom_add_vocab_form');
return $output;
}
/*
* The form
*/function le_custom_add_vocab_form() {
$form = array();
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Word or Short Phrase'),
'#default_value' => '',
'#size' => 40,
'#maxlength' => 60,
'#required' => TRUE,
'#description' => t('Enter the Spanish word or short phrase to add as a Vocabulary Item');
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Continue')
);
return $form;
}
function le_custom_add_vocab_form_submit($form, &$form_state) {
$view = views_get_view('vocab_of_title');
$view->set_display('default');
$view->set_arguments(array($title));
$view->is_cacheable = FALSE;
$view->execute();
if (count($view->result)) {
$message = t('One or more Vocabulary Items already exist called "@title".', array('@title' => $title));
drupal_set_message($message, 'warning', FALSE);
$form_state['redirect'] = 'add-vocab/' . urlencode($title);
} else {
// Stash away the new word in the session. Keep the URL's clean
// and shiny, as well as peventing coming to the new node page w/o
// doing this the first steps
$_SESSION['le_custom'] = array( 'new_word' => $title );
$form_state['redirect'] = array('node/add/vocab');
}
}?><?php/*
* Page informing user that there are existing vocab item(s) with same
* title listing them and asking for confirmation.
*/function le_custom_confirm_new_vocabitem($title) {
drupal_set_title( t('Create Vocabulary Item "@title"?', array('@title' => $title)) );
$view = views_get_view('vocab_of_title');
$view->set_display('default');
$view->set_arguments(array($title));
$view->is_cacheable = FALSE;
$view->execute();
if (count($view->result)) {
$output = '<p>' . t('Is the Vocabulary Item you would like to enter similar in meaning to an item listed below?') . '</p>';
// ... I've snipped out some other text ...
$output .= $view->render();
$output .= drupal_get_form('le_custom_confirm_new_vocabitem_form', $title);
return $output;
} else {
$_SESSION['le_custom'] = array( 'new_word' => $title );
drupal_goto('node/add/vocab');
}
}
/*
* The form used by above page
*/function le_custom_confirm_new_vocabitem_form(&$form_state, $title) {
$form = array();
// Stash away the title so it's available to submit function
$form['vocab_title'] = array(
'#type' => 'value',
'#value' => $title
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Continue')
);
$form['cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel')
);
return $form;
}
/*
* Submit function for confirmation page. Go to add node page,
* or cancel to some other page....
*/function le_custom_confirm_new_vocabitem_form_submit($form, &$form_state) {
$title = $form_state['values']['vocab_title'];
if ($form_state['values']['cancel'] == $form_state['clicked_button']['#value']) {
$form_state['redirect'] = 'add-vocab';
} else {
$_SESSION['le_custom']['new_word'] = $title;
$form_state['redirect'] = array('node/add/vocab');
}
}?>On the duplicates warning page, I don't just check to see if there are duplicates, I actually display them. Fortunately, Views has done the work of generating an HTML table for me. Lets take a look at that view:

It's a very basic View. It filters nodes to those of the Vocabulary Item type, and takes a title as an argument, which further filters those nodes down to just those with the title specified in the argument. Two fields are output: the title, and the short translation for the Vocabulary Item (e.g. a large tasty bird). The style is "table", so the output is formatted as an HTML table.
On the warning page's submit function, the page redirection will depend on which button the user clicked. Cancel will go back to the title entry page, and continue will, as we've seen twice before now, stash away the title in a session variable, and redirect the the node add form.
So now we see how the first two pages work. A title is collected, the user is warned if there are already Vocabulary Item nodes with the given title, and then taken to the node add form with the title stashed away in the session.
So, let's take a look at the code that tweaks the node add form to our suit my purpose. I use hook_form_alter() like this:
<?phpfunction le_custom_form_alter(&$form, $form_state, $form_id) {
if ($form_id == 'vocab_node_form') {
$title_access = user_access('edit vocab titles');
// If the use does not have permission to edit vocab titles, hide the element, and add
// a validation function to check that the title has not changed.
if (!$title_access) {
$form['#validate'][] = 'le_custom_vocab_node_form_validate';
$form['title']['#type'] = 'hidden';
}
// if this a form for a new node, then prefill title with the
// string supplied in a session variable
if (empty($form['#node']->nid)) {
if (!empty($_SESSION['le_custom']['new_word'])) {
$new_word = $_SESSION['le_custom']['new_word'];
$form['title']['#default_value'] = $new_word;
if (!$title_access) {
drupal_set_title( t('Create "@title"', array('@title' => $new_word) ) );
}
} else if (!$title_access) {
// If the title isn't provided in the session, and user does not have
// permission to alter titles, redirect to collect a title.
drupal_goto('add-vocab');
}
}
}
}?><?phpfunction le_custom_perm() {
return array('edit vocab titles');
}?><?phpfunction le_custom_vocab_node_form_validate($form, &$form_state) {
if ($form_state['values']['title'] != $form['title']['#default_value']) {
form_set_error('title', t('You are not allowed to change the title.'));
}
}?>If the user is just editing the form, there are no other changes that need to be made. The standard node form title (which is the title of the node), is fine. If this is a new node, we need to grab the title from the session variable, and set the form's title field with it, as well as change the title of the page from the standard "Create Vocab Item", so that the user knows the title of the Vocabulary Item node they are creating.
If the user tries to come to the node add page, and the session variable for the title is not set, we'll send them to the title collection page:
<?php
else if (!$title_access) {
// If the title isn't provided in the session,and user does
// not have permission to alter titles, redirect to collect a title.
drupal_goto('add-vocab');
}?><?phpfunction le_custom_init() {
// our stored variable for new node title only lasts when going to the next page, then it's cleared.
if (!empty($_SESSION['le_custom']['new_word']) && strcmp('node/add/vocab', $GLOBALS['_GET']['q']) != 0) {
unset($_SESSION['le_custom']['new_word']);
}
}?>It's probably possible to build this up into a configurable contributed module, where the administrator specifies the node types for which they would like to restrict duplicate titles, and provides a view that does the work of checking and listing duplicates. I will leave that as an exercise for the reader.
No comments:
Post a Comment