Defining Custom Node Content Types In A Module
on
Defining Custom Node Content Types In A Module
What're you crazy!? Haven't you ever heard of the CCK module? Yes, it's true CCK coupled with views will allow one to create, manipulate and display custom content. Combined with hook_nodeapi, the cck/views combo is can be extremely powerful and flexible. 99% of the time these two modules are sufficient to handle the job of building and managing custom content. The problem arises when you need any sort of custom access control or ownership of your custom node content. With CCK, the ownership of the node belongs to node.module, and there's no 'access' operator exposed in hook_nodeapi. You could patch core ( http://drupal.org/node/143075 ), but in general, that's not really a road you want to start down.
Write your own damn module
The good news is that you can write a module to define a content type. I know it may sound crazy to some of you, but this is how it had to be done before CCK and views came along, and as I mentioned before this method of defining content types is sometimes preferable to using the CCK/Views method.
The scenario
Let's say you're building a site that will act as a resource for publicists. You want to be able to store press outlets from all around the world. The idea would be that a publicist can come onto this site, enter information about a press outlet including the email address of the Head Editor of that press outlet. You want the publicist who enters the press outlet data to be the owner of that data. While other users should be able to view the data, you don't want them to have access to edit it. In addition, once the press outlet information is saved, you want to send an email to the Head Editor of that outlet giving he or she the option of registering for your site and taking over ownership of that press outlet. For example: I enter in the information for a newsletter called 'Bow Ties Weekly'. The Editor's email address is 'editor@bowtiesweekly.com'. Once I save that information, I'll be able to edit it, while all the users of the site will be able to search for it and view it. The site sends an email to 'editor@bowtiesweekly.com' providing her with a link to register. If she does register, then she will be given the option to assume owership of her weekly. This sort of workflow is very difficult to achieve with CCK/Views, but not so bad at all with a custom content module.
Getting started
Like any other custom module, this one will need a .info file.
; $Id$
name = Press
description = "Defines the Press content type."
package = Examples
core = 6.x t('Table to hold Press data'),
'fields' => array(
'vid' => array(
'description' => t('Revision id'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'nid' => array(
'description' => t('Node id'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'press_contact_email' => array(
'description' => t('The main contact at the press outlet (the editor)'),
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
),
'press_owner_uid' => array(
'description' => t('The uid of the user who currently has "ownership" of this press outlet.'),
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'press_owner_mail' => array(
'description' => t('The email associated with the person who currently has "ownership" of this press outlet.'),
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
),
'primary key' => array('vid', 'nid'),
'indexes' => array(
'press_nid' => array('nid'),
),
);
return $schema;
}
function press_install() {
// Create the tables
drupal_install_schema('press');
drupal_set_message("The press table has been created");
}
function press_uninstall() {
// Drop the tables
drupal_uninstall_schema('press');
drupal_set_message("The press table has been deleted");
}The module
You'll want a hook_help:
/**
* Implementation of hook_help()
*/
function press_help($path, $arg) {
switch ($path) {
case 'admin/modules#description':
return t('Defines the press content type');
}
}function press_node_info() {
return array(
'press' => array(
'name' => t('press'),
'module' => 'press',
'description' => t("Defines the press content type."),
'has_title' => TRUE,
'title_label' => t('Name'),
'has_body' => FALSE,
),
);
}/**
* Implementation of hook_access()
*/
function press_access($op, $node, $account) {
if ($op == 'create') {
return user_access('create press', $account);
}
if ($op == 'update') {
if (user_access('edit any press', $account) || (user_access('edit press you have system ownership of', $account) && user_owns_press($account->uid, $node->nid))) {
return TRUE;
}
}
if ($op == 'delete') {
if (user_access('delete any press', $account) || (user_access('delete press you have system ownership of', $account) && user_owns_press($account->uid, $node->nid))) {
return TRUE;
}
}
}
/**
* Implementation of hook_perm()
*/
function press_perm() {
return array(
'create press',
'delete press you have system ownership of',
'delete any press',
'edit press you have system ownership of',
'edit any press',
);
}/**
* Implementation of hook_menu().
*/
function press_menu() {
$items = array();
$items['press/%/transfer'] = array(
'title' => 'Take ownership of your press',
'page callback' => 'press_ownership_transfer',
'page arguments' => array(1),
'type' => MENU_CALLBACK,
'access arguments' => array('access content'),
);
return $items;
}/*
* Implementation of hook_block()
*/
function press_block($op = 'list', $delta = 0) {
global $user;
$block = array();
switch ($op) {
case 'list':
$block[0]['info'] = t('Take Control');
$block[1]['info'] = t('My press nodes');
$block[2]['info'] = t('Orphaned press nodes');
return $block;
case 'view':
switch ($delta) {
case 0:
// We only want to show this block if there are press nodes that this user can take control of. If the user's email is the press_contact_email, but they aren't the current owner, then offer them the job.
$count = db_result(db_query("SELECT count(*) FROM {press} v WHERE v.press_contact_email = '%s' AND v.press_owner_uid <> %d", $user->mail, $user->uid));
if ($count > 0) {
$block['subject'] = t('Take Control');
$block['content'] = t('Our users have told us that we have press entries in our system that you should have ownership of! Please click the press name below to assume ownership of that press entry. <br /><br />');
$block['content'] .= '';
$result = db_query("SELECT n.title, n.nid FROM {press} v LEFT JOIN {node} n ON n.vid = v.vid WHERE v.press_contact_email = '%s' AND v.press_owner_uid <> %d", $user->mail, $user->uid);
while ($row = db_fetch_object($result)) {
$block['content'] .= '
<li>'. l($row->title, 'press/'. $row->nid .'/transfer');
}
$block['content'] .= '
';
}
break;
case 1:
// We only want to show this block if there are press nodes owned by this user AND if the user is not the admin user.
// We have a different block to show him (Orphaned press nodes)
if ($user->uid != 1) {
$count = db_result(db_query("SELECT count(*) FROM {press} v WHERE v.press_contact_email = '%s' AND v.press_owner_uid = %d", $user->mail, $user->uid));
if ($count > 0) {
$block['subject'] = t('My press nodes');
$block['content'] = t('Click any of the links below to view and edit your press information <br /><br />');
$result = db_query("SELECT n.title, n.nid FROM {press} v LEFT JOIN {node} n ON n.vid = v.vid WHERE v.press_contact_email = '%s' AND v.press_owner_uid = %d", $user->mail, $user->uid);
while ($row = db_fetch_object($result)) {
$block['content'] .= ''. l($row->title, 'node/'. $row->nid .'/edit');
}
$block['content'] .= '';
}
}
break;
case 2:
// This block will show the admin any orphaned press nodes.
if ($user->uid == 1) {
// Press nodes where the owner no longer exists in our system.
$count = db_result(db_query("SELECT count(*) from {press} v LEFT JOIN {node} n ON n.vid = v.vid WHERE v.press_owner_uid NOT IN (SELECT uid FROM {users})"));
if ($count > 0) {
$block['subject'] = t('Orphaned press nodes');
$block['content'] = t('These press nodes no longer have a valid owner <br /><br />');
$result = db_query("SELECT n.title, n.nid FROM {press} v LEFT JOIN {node} n ON n.vid = v.vid WHERE v.press_owner_uid NOT IN (SELECT uid FROM {users})");
while ($row = db_fetch_object($result)) {
$block['content'] .= ''. l($row->title, 'node/'. $row->nid .'/edit');
}
$block['content'] .= '';
}
}
break;
}
return $block;
}
}/**
* Implementation of hook_form()
*/
function press_form(&$node) {
global $user;
// The site admin can decide if this node type has a title and body, and how
// the fields should be labeled. We need to load these settings so we can
// build the node form correctly.
$type = node_get_types('type', $node);
if ($type->has_title) {
$form['title'] = array(
'#type' => 'textfield',
'#title' => check_plain($type->title_label),
'#required' => TRUE,
'#default_value' => $node->title,
'#weight' => -5
);
}
// There is no body for this node, so we don't need to worry about that
// Define form elements specific to this node type here:
// Define hidden elements first
if ($user->uid == 1) {
if ($node->press_owner_uid != '') {
$form['press_owner_uid'] = array(
'#type' => 'item',
'#title' => 'press Owner UID',
'#value' => isset($node->press_owner_uid) ? $node->press_owner_uid : '',
);
}
if ($node->press_owner_mail != '') {
$form['press_owner_mail'] = array(
'#type' => 'item',
'#title' => 'press Owner Email',
'#value' => isset($node->press_owner_mail) ? $node->press_owner_mail : '',
);
}
}
else {
$form['press_owner_uid'] = array(
'#type' => 'hidden',
'#value' => isset($node->press_owner_uid) ? $node->press_owner_uid : '',
);
$form['press_owner_mail'] = array(
'#type' => 'hidden',
'#value' => isset($node->press_owner_mail) ? $node->press_owner_mail : '',
);
}
// Now for the public fields:
$form['press_contact_email'] = array(
'#type' => 'textfield',
'#title' => t('Email'),
'#default_value' => isset($node->press_contact_email) ? $node->press_contact_email : '',
);/**
* Implementation of hook_insert()
*/
function press_insert($node) {
global $user;
// Since we don't want to trust a user to enter a press_owner_uid or press_owner_mail,
// we need to do that programatically.
$press_owner_uid = $user->uid;
$press_owner_mail = $user->mail;
db_query("INSERT INTO {press}
(vid, nid, press_contact_email) VALUES
(%d, %d, %s')",
$node->vid, $node->nid, $node->press_contact_email);
// TODO: Email the press_contact_email and invite them to assume control of this press outlet
}
/**
* Implementation of hook_update
*/
function press_update($node) {
// If this is a new node or we're adding a new revision:
if ($node->revision) {
press_insert($node);
}
else {
db_query("UPDATE {press} SET press_contact_email = '%s' WHERE vid = %d",
$node->press_contact_email, $node->vid);
}
}
/**
* Implementation of hook_nodeapi()
*/
function press_nodeapi(&$node, $op, $teaser, $page) {
switch ($op) {
case 'delete revision':
db_query('DELETE FROM {press} WHERE vid = %d', $node->vid);
break;
}
}
/**
* Implementation of hook_delete()
*/
function press_delete($node) {
db_query('DELETE FROM {press} WHERE nid = %d', $node->nid);
}
/**
* Implementation of hook_load()
*/
function press_load($node) {
$additions = db_fetch_object(db_query("SELECT press_contact_email
FROM {press} WHERE vid = %d", $node->vid));
return $additions;
}
/**
* Implementation of hook_view()
*/
function press_view($node, $teaser = FALSE, $page = FALSE) {
$node = node_prepare($node, $teaser);
$node->content['custom_fields'] = array(
'#value' => theme('press', $node),
'#weight' => 1,
);
return $node;
}/**
* Implementation of hook_theme()
*/
function press_theme() {
return array(
'press' => array(
'arguments' => array('node'),
),
);
}
/**
* A custom theme function
*/
function theme_press($node) {
global $user;
$output = '
<div class="press">';
$output .= $node->phone ."<br />". $node->website ."<br />". $node->address_1 ."<br />".
$node->address_2 ."<br />". $node->city ."<br />". $node->state_province ."<br />".
$node->country ."<br />". $node->capacity ."<br />". $node->age_restrictions ."<br />". $node->tix_by_phone ."<br />".
$node->tix_by_web ."<br />". $node->press_contact ."<br />". $node->press_contact_phone_1 ."<br />".
$node->press_contact_phone_2 ."<br />". $node->press_contact_email ."<br />". $node->press_notes ."<br />";
if ($user->uid == 1) {
$output .= $node->press_owner_uid ."<br />". $node->press_owner_mail ."<br />";
}
$output .= '</div>
';
return $output;
}/**
* Helper & Callback functions
*
*/
/*
* Callback function for 'press/$nid/transfer' where $nid is a press node's node id
*/
function press_ownership_transfer($nid) {
global $user;
$press = node_load($nid);
db_query("UPDATE {press} SET press_owner_uid = %d, press_owner_mail = '%s' WHERE vid = %d", $user->uid, $user->mail, $press->vid);
drupal_set_message("You have assumed ownership of ". $press->title .". Click the links in the 'My press nodes' section to review your press information.");
drupal_goto();
return;
}
/*
* Function used by hook_access to determine whether a given user is
* a press owner in the system.
*
* Takes a uid and an nid as a parameters.
*/
function user_owns_press($uid, $nid) {
$user = user_load($uid);
$press = node_load($nid);
if ($press->type != 'press') {
return FALSE;
}
else if ($user->mail == $press->press_owner_mail && $press->press_owner_mail == $press->press_contact_email && $press->press_owner_uid == $user->uid) {
return TRUE;
}
return FALSE;
}And that's it. Now you should be able to create and manage press nodes with powerful access control built-in.
Thanks for sharing this very interesting post on this wealth of coding information.
To learn PHP,mysql,ajax,html,javascript,jquery with advanced concepts, you can visit <a href="http://advancedphptutorial.blogspot.com">Advanced PHP tutorial </a>
With the messed up code format it might be a good idea to just attach the module to this post, or to put it on your d.o sandbox?
You will also likely want to expose your fields to Views. CCK does it automatically, but you can do it too: just implement hook_views_api() and hook_views_data() to define your custom fields and, as long as you use standard field handlers for them, you will be able to use them in Views, and not be restricted to the core properties or CCK fields added after the fact as Yched suggests.
Detailed info is available in the Views advanced_help, and on http://views.doc.logrus.com/
Just for the record : defining your own node type in a custom module doesn't mean you lose the benefit of CCK. Your content type can receive CCK fields just like any other.
In fact, CCK has nothing to do with creating custom node types in the UI, this is done by core (and those node tpes are therefore 'owned' by node.module). CCK acts on any node type, independantly of how it was created.
You might want to go back and unescape the HTML..
Eric







