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 ‘[email protected]’. 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 ‘[email protected]’ 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

You’ll also need a .install file to create the database table that will store the press attributes. I’ve simplified this one for the sake of this walk through.

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()
function press_uninstall()

The module

You’ll want a hook_help:

/**
* Implementation of hook_help()
*/
function press_help($path, $arg)
}

hook_node_info is required:

function press_node_info()

I’ve set the ‘has_body’ attribute to FALSE just for the purposes of this walkthrough. To have a node body defined for your content, just change it to TRUE.
Now we’ll set up hook_access and hook_perm:

/**
* Implementation of hook_access()
*/
function press_access($op, $node, $account)
if ($op == 'update')
}
if ($op == 'delete')
}
}
/**
* Implementation of hook_perm()
*/
function press_perm()

Since we want to provide press contacts with a way to take ownership of their magazine, or newspaper or whatever, we’ll need to set up a hook_menu to register the path for the callback function that will handle that transfer:

/**
* Implementation of hook_menu().
*/
function press_menu()

We’ll want a way to display to the user which press outlets they own and which ones they have the option of owning. For this walk through, I’ve decided to use a hook_block, but you could use a custom page by returning another path from hook_menu.

/*
* Implementation of hook_block()
*/
function press_block($op = 'list', $delta = 0) 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.

‘);
$block[‘content’] .= ‘

';
$result = db_query("SELECT n.title, n.nid FROM v LEFT JOIN 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'] .= '
';
}
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) v WHERE v.press_contact_email = '%s' AND v.press_owner_uid = %d", $user->mail, $user->uid));
if ($count > 0) v LEFT JOIN 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) v LEFT JOIN n ON n.vid = v.vid WHERE v.press_owner_uid NOT IN (SELECT uid FROM )"));
    if ($count > 0) v LEFT JOIN n ON n.vid = v.vid WHERE v.press_owner_uid NOT IN (SELECT uid FROM )");
    while ($row = db_fetch_object($result)) {
    $block['content'] .= '
  • '. l($row->title, 'node/'. $row->nid .'/edit');
    }
    $block['content'] .= '';
    }
    }
    break;
    }
    return $block;
    }
    }

    You’ll need a hook_form to define form fields for the creation and editing of the press node:

    /**
    * Implementation of hook_form()
    */
    function press_form(&$node)
    // 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_mail != '')
    }
    else
    // 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 : '',
    );

    Now we’ll need to handle the INSERT, UPDATE and DELETE, DELETE REVISION, LOAD and VIEW operations of our new node type. Drupal does a lot of the work, but we’ll need to define some hooks to do our fair part:

    /**
    * Implementation of hook_insert()
    */
    function press_insert($node)
    (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)
    else 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) WHERE vid = %d', $node->vid);
    break;
    }
    }
    /**
    * Implementation of hook_delete()
    */
    function press_delete($node) WHERE nid = %d', $node->nid);
    }
    /**
    * Implementation of hook_load()
    */
    function press_load($node) WHERE vid = %d", $node->vid));
    return $additions;
    }
    /**
    * Implementation of hook_view()
    */
    function press_view($node, $teaser = FALSE, $page = FALSE)

    Notice that for VIEW I’m calling a theme function for the press node. We’ll need to define hook_theme to register that theme and then of course we’ll need to write the theme function itself.

    /**
    * Implementation of hook_theme()
    */
    function press_theme()
    /**
    * A custom theme function
    */
    function theme_press($node)
    $output .= '

    ‘;
    return $output;
    }

    Finally we’ll need a couple of callback and helper functions defined:

    /**
    * Helper & Callback functions
    *
    */
    /*
    * Callback function for 'press/$nid/transfer' where $nid is a press node's node id
    */
    function press_ownership_transfer($nid) 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)
    else if ($user->mail == $press->press_owner_mail && $press->press_owner_mail == $press->press_contact_email && $press->press_owner_uid == $user->uid)
    return FALSE;
    }

    And that’s it. Now you should be able to create and manage press nodes with powerful access control built-in.