invalid command name "ns_adp_include"
    while executing
"ns_adp_include header.adp "
    invoked from within chunk: 2 of adp: /home/dvr/web/dvr/cmd/custom-handlers.adp

A well-designed CMS will have two distinct parts: (1) a set of core services that support all types of content, and (2) small pieces of custom code that plug into the core and support specific content types. The code that translates a url to a revision_id is in the first type; the code that fetches the content fields for a news story and formats them in a way specific for news stories is the second. Adding a new content type shouldn't require modifications to the core tables or plsql.

Beyond the extension tables that hold a type's custom fields, adding a new type requires 6 small pieces of code:

  1. One piece that fetches the custom fields and formats them for display to the public.
  2. One piece that displays all the custom fields to an administrator.
  3. One piece that generates a web form to edit the custom fields
  4. One piece that knows how to update the type's custom fields
  5. One piece that knows how to create a new item for the type
  6. One piece that knows how to create a new revision of this type

These are called "handlers" because they plug into the rest of the system and handle specific tasks for their content type. If the CMS receives a request for /publish/foo/bar and determines this is the url for a story, it will first verify the user has access to view the story (an operation that's the same for all content items), then hand off control to a custom handler that fetches/formats the content and writes it out to the user.

A developer writing a custom handler doesn't have the burden of implementing the system's rules. For example, the handler that updates the content type's custom fields doesn't need to verify the user has the item checked out; the handler will be called only after the CMS has verified it's the right user. The handler also won't need to update the revision's audit fields because these fields will have already been updated. Similarly, the handler to generate the edit form will only be called if the user has the item checked out, and the "public view" handler will only be called if this user has permission to view the story.

The first four handlers will be written in our scripting language. Here is a summary of each:

  1. The public view handler will be used in two places: first, to show the live revision to the public, and second, to generate a preview for staff members.
  2. The staff view handler displays the content type's custom fields in a way that makes sense to administrators (see screenshot).
  3. The edit form handler will be called if the user viewing the page has the content checked out (see screenshot).
  4. The update handler will be used in conjunction with the "edit form" handler. It will be called after the CMS has verified this user has the content checked out and after the audit fields have been updated.

These four handlers will be grouped together in some way that makes sense for the language being used. In Java, they'll be four static methods in the same class (and the name of the class will be stored in cm_types). If a scripting language like php is used, the easiest solution is to follow a naming convention that uses the type's type_key in the function name -- for example, the handler to update a story's type (type_key = 'story') would be cm_update_story.

The last two handlers need to be written in plsql because they will be called from within the core plsql:

  1. The create item handler will be called whenever a new item is created.
  2. The create revision handler will be called whenever somebody checks out an item and a new revision needs to be created.

#1: The "public view" handler

All public content will be served from beneath /publish/. If the editor chooses /bush/interview.html as the url, the public url will be /publish/bush/interview.html. Staff members will be able to preview the most recent version at /preview/bush/interview.html.

All requests to /publish* and /preview* will be handled by cm_php cm_public_url_handler, which looks at the url to choose the correct revision, determines the class for that of content, then calls the type's custom handler.

function cm_public_url_handler () {

    $full_url = $_SERVER['REQUEST_URI'];

    // If the requested url is /live/foo/bar, we want to split it
    // to 'live' and /foo/bar'

    if (!preg_match("/^\/(live|preview)(\/.*)/", $full_url, $url_parts)) {

        cm_die_not_found();

    } else {

        $mode = $url_parts[1];
        $url  = $url_parts[2];

        $bind_vars = array();

        if ($mode == 'preview') {

            cm_in_preview_mode_p(1);

            // Only staff members are allowed to use the preview
            // mode. If this person isn't on staff, don't let them go
            // any further.

            cm_restrict_to_staff();

            // Staff members are allowed to preview any revision.
            // By default, they preview the last revision, but they
            // can specify a revision_id.

            $chosen_revision_id = form_value('revision_id');

            if ($chosen_revision_id != '') {
                // They're choosing the revision

                $content_view = 'cm_v_content';
                $where_clause = "revision_id = :revision_id";

                $bind_vars['revision_id'] = $chosen_revision_id;

            } else {
                // Choose the last revision
                
                $content_view = 'cm_vlast_content';
                $where_clause = "item_id = (select cm_item_for_url(:url) from dual)";

                $bind_vars['url'] = $url;
            }

        } else {
            // They're asking for the live revision

            $content_view = 'cm_vlive_content';
            $where_clause = "item_id = (select cm_item_for_url(:url) from dual)";

            $bind_vars['url'] = $url;
        }

        $db = db_open();
        
        $q = db_prepare($db, "
            select
                type_key,
                directory_id,
                revision_id
            from $content_view
            where $where_clause");
            
        foreach($bind_vars as $bind_name => $bind_value) {
            db_bind($q, $bind_name, $bind_value);
        }

        db_execute($q);

        if (db_getrow($q)) {

            $type_key     = db_getcol($q, 'type_key');
            $revision_id  = db_getcol($q, 'revision_id');

            cm_page_info(directory_id, db_getcol($q, 'directory_id'));

            db_close($db);

            // Figure out the name of the content type's handler
            $handler_name = "cm_write_public_$type_key";

            // Call the handler
            $handler_name($revision_id);

        } else {
            // a revision doesn't exist

            db_close($db);

            cm_die_not_found();
        }
    }
}

If the item is a story, the result will be a call to cm_write_public_story, which fetches the correct fields, formats them, and writes them to the user:

function cm_write_public_story ($revision_id) {

    $db = db_open();

    $q = db_prepare($db, "
        select
            s.headline,
            s.subhead,
            s.body,
            a.template_filename
        from cmx_r_story s, cm_v_templates_assigned a
        where s.revision_id = :revision_id
        and s.revision_id = a.revision_id(+)");

    db_bind($q, revision_id, $revision_id);

    db_execute($q);
    db_getrow($q);

    foreach(db_colnames($q) as $colname) {
        $$colname = db_getcol($q, $colname);
    }

    $body = cm_process_tags_in_text($body);

    cm_page_info("page_title", $headline);

    db_close($db);

    header('Content-Type: text/html');

    if ($template_filename)
        include(cm_template_dir() . '/' . $template_filename);
    else
        echo '(no template)';
}

#2 & #3: The "staff view" and "edit form" handlers

A staff member looking at an item on the staff pages will see a form to edit a item if (1) it's the last revision and (2) the item is checked out to them; otherwise they'll see the staff view of the revision. The admin page will print the header fields common to all content, then call one of the type's custom handlers.

If the system calls the "edit form" handler, the user will see a page like:

The "edit form" handler generates the light gray part of the page. The fields in the dark gray are common to all content types, so no new code needs to be written for that part of the form.

The source for this handler:

function cm_edit_form_story ($revision_id) {

    $db = db_open();

    $q = db_prepare($db, "
        select
            headline,
            subhead,
            body
        from cmx_r_story
        where revision_id = :revision_id");

    db_bind($q, revision_id, $revision_id);

    db_execute($q);
    db_getrow($q);

    foreach (db_colnames($q) as $colname) {
        $$colname = db_getcol($q, $colname);
    }

    // Generate the form value html, i.e., VALUE="..."'
    $fv_headline = form_value_html_for_value(db_getcol($q, headline));

    // Generate the html that goes between the TEXTAREA tags.
    $ta_subhead = form_textarea_value_html_for_value(db_getcol($q, subhead));
    $ta_body    = form_textarea_value_html_for_value(db_getcol($q, body));

    db_close($db);

    return "
    <table cellspacing=5>
      <tr>
        <th align=left>Headline</th>
        <td><input name=headline size=50 $fv_headline></td>
      </tr>
      <tr valign=top>
        <th align=left>Subhead</th>
        <td><textarea name=subhead cols=40 rows=2>$ta_subhead</textarea></td>
      </tr>
      <tr>
        <td colspan=2>
        <b>Body</b><br>
        <textarea name=body cols=50 rows=20>$ta_body</textarea>
        </td>
      </tr>
    </table>";
}

If the user doesn't have it checked out, or it isn't the last revision, they see the "admin view":

The "admin view" handler generates the light gray part of the page.

The source:

function cm_admin_view_story ($revision_id) {

    $db = db_open();

    $q = db_prepare($db, "
        select
            headline,
            subhead,
            body
        from cmx_r_story
        where revision_id = :revision_id");

    db_bind($q, revision_id, $revision_id);

    db_execute($q);
    db_getrow($q);

    $headline = db_getcol($q, headline);
    $subhead  = db_getcol($q, subhead);

    // We want to show the html source for the body, so escape it and 
    // word-wrap it.

    $body_fm = wordwrap(htmlspecialchars(db_getcol($q, body)), 60);

    db_close($db);

    return "
    <table cellpadding=5>
      <tr>
        <th align=left>Headline</th>
        <td>$headline</td>
      </tr>
      <tr>
        <th align=left>Subhead</th>
        <td>$subhead</td>
      </tr>
      <tr valign=top>
        <th align=left>Body</th>
        <td><pre>$body_fm</pre></td>
      </tr>
    </table>";
}

#4: The update handler

The update handler receives values from the "edit form" handler and writes them to the database.

For the story type, the update handler is cm_update_story

function cm_update_story ($revision_id) {

    $db = db_open();

    $q = db_prepare($db, "
        update cmx_r_story set
            headline    = :headline,
            subhead     = :subhead,
            body        = :body
        where revision_id = :revision_id");

    db_bind($q, revision_id, $revision_id);

    db_bind($q, headline, form_value(headline));
    db_bind($q, subhead,  form_value(subhead));
    db_bind($q, body,     form_value(body));

    db_execute($q);

    db_close($db);
}

This handler doesn't need to update audit fields or verify that the user is allowed to update this revision because the handler is called within a block of code like:

db_transaction_begin($db);

// Try to update the audit fields. Throw an exception if the
// user isn't allowed.

$q = db_prepare($db, "
  begin
    cm_revision_update (
        p_revision_id => :revision_id,
        p_user_id     => :caller_id
    );
  end;");

db_bind($q, revision_id,      $revision_id);
db_bind($q, caller_id,        $caller_id);

// This executes the plsql block above and checks for any 
// errors returned. If it determines the error is a message from
// the core, it displays the error to the user.

cm_core_db_execute($q);

// Choose the handler to call
$update_handler_name = "cm_update_" . $type_key;

// Call the handler
$update_handler_name($revision_id);

// If the call to the handler completes without error, commit
// everything.

db_transaction_end($db);

cm_revision_update verifies the user has the item checked out and it's the item's last revision being updated. If either check fails, an exception is throw and the handler is never called. If both checks pass, the audit fields are updated and the handler runs. If the handler dies with an error, the update to the audit fields will be rolled back because the call to cm_transaction_end that commits the update will never be called.

#5 & #6: The "create item" and "create revision" handlers

The last two handlers are written in plsql because they need to be called from within the core plsql. This ensures that the inserts into the extension tables are done in the same transaction as the inserts into cm_items and cm_revisions, and if any of these inserts fail for some reason, we're not left with a half-entered piece of content (i.e., a revision with audit information, but no content).

The "create revision" handler is called from within cm_revision_create:

create or replace function cm_revision_create (
    --
    -- Create a new revision based on the previous revision for
    -- this item.
    -- 
    p_item_id               in integer, 
    p_user_id               in integer,
    p_state_key             in cm_workflow_states.state_key%type
) return integer
as
    v_revision_id           integer;
    v_revision_to_copy      integer;
    v_type_key              cm_types.type_key%type;
begin

    select revision_id, type_key 
      into v_revision_to_copy, v_type_key
    from cm_vlast_content
    where item_id = p_item_id;

    select cm_sequence.nextval into v_revision_id from dual;

    insert into cm_revisions (
        revision_id, item_id, publication_date, template_id,
        audit_user, audit_creation_date,
        state_key, state_audit_user, state_audit_date)
    select
        v_revision_id, p_item_id, publication_date, template_id,
        p_user_id, sysdate,
        p_state_key, p_user_id, sysdate
    from cm_revisions
    where revision_id = v_revision_to_copy;

    execute immediate '
        begin 
            cm_revision_create_' || v_type_key || '(:1, :2); 
        end;' 
        using v_revision_id, v_revision_to_copy;

    return v_revision_id;

end;

The 'execute immediate' block is what calls the custom handler. Since the name of the handler is determined at run-time, we build up the plsql block like a string, then evaluate the string by feeding it to 'execute immediate'. In the case of a story, we're calling cm_revision_create_story.

The "create revision" handler takes two arguments: the revision_id of the new revision, and the revision_id of the revision whose values should be copied to populate the new revision. The source for cm_revision_create_story is:

create or replace procedure cm_revision_create_story (
    p_revision_id       in integer,
    p_revision_to_copy  in integer
)
as
begin

    insert into cmx_r_story (revision_id, headline, subhead, body)
    select p_revision_id, headline, subhead, body
    from cmx_r_story
    where revision_id = p_revision_to_copy;

end;

The second handler, cm_item_create_type_key is called when an item is created:

create or replace procedure cm_item_create_story (
    p_item_id           in integer,
    p_revision_id       in integer
)
as
begin
    insert into cmx_r_story (revision_id) values (p_revision_id);
end;

which is called from within cm_item_create:

create or replace function cm_item_create (
    --
    -- Create a new content item and return the revision_id
    -- 
    p_item_id               in integer default null,
    p_type_key              in cm_types.type_key%type,
    p_department_id         in integer,
    p_short_name            in cm_items.short_name%type default null,
    p_state_key             in cm_workflow_states.state_key%type default 'created',
    p_user_id               in integer
) return integer
as
    v_item_id               integer := p_item_id;
    v_revision_id           integer;
    v_name_taken_p          integer;
    v_type_key              cm_types.type_key%type;
begin

    cm_verify_not_null(p_department_id, 'A department is required');

    cm_department_verify_allowed (
        p_department_id   => p_department_id,
        p_user_id         => p_user_id,
        p_action          => 'add_item',
        p_error           => 'You''re not allowed to add an item to this department'
    );

    if v_item_id is null then
        select cm_sequence.nextval into v_item_id from dual;
    end if;
 
    if p_short_name is not null then

        select count(*) into v_name_taken_p
        from cm_items
        where short_name = p_short_name;

        if v_name_taken_p = 1 then 
            cm_throw_exception('That name is already taken');
        end if;
    end if;

    insert into cm_items (
        item_id, type_key, department_id, short_name, audit_user
    ) values (
        v_item_id, p_type_key, p_department_id, p_short_name, p_user_id
    );

    select cm_sequence.nextval into v_revision_id from dual;

    insert into cm_revisions (
        revision_id, item_id, audit_user, audit_creation_date,
        state_key, state_audit_user, state_audit_date
    ) values (
        v_revision_id, v_item_id, p_user_id, sysdate,
        p_state_key, p_user_id, sysdate
    ); 

    cm_verify_state_allowed (
        p_revision_id => v_revision_id,
        p_user_id     => p_user_id,
        p_state_key   => p_state_key
    );

    execute immediate '
        begin 
            cm_item_create_' || p_type_key || '(:1, :2); 
        end;' 
        using v_item_id, v_revision_id;

    return v_revision_id;

end cm_item_create;
invalid command name "ns_adp_include"
    while executing
"ns_adp_include footer.adp "
    invoked from within chunk: 12 of adp: /home/dvr/web/dvr/cmd/custom-handlers.adp