Restricting CCK forms in Drupal 6

This article contains a programming howto for stripping unwanted fields from forms generated by the CCK module in Drupal 6. It is natural for a content type to gather as many fields as the logical data schema requires (essentially, the decision about adding a field to an existing content type or creating a new content type, is dictated by considerations about data cardinality and independence in life cycles of individual pieces of data). Real-world applications will usually require that certain subsets of these fields should not be editable by everyone, or that they should only be editable at certain times or when certain conditions are met (for example, based on another field's value within the node being edited). Understandably, CCK doesn't know anything about such special needs out-of-the-box, and so it will render all node fields as equals on a common form.

Consider a case where you have already created a complex default form for editing the entire content type, possibly with field groups and a custom theme which renders them in a fancy hierarchical way. Now let's say you have another user role which is only supposed to understand and edit a few fields from the said content type. You want a new form which displays only those fields and saves the submitted values to the node, validating them in exactly the same way as they would be in the full-node edit scenario. To make things even more interesting, suppose that one of the fields is a select box for which you want to restrict the list of allowed values when it is presented on the restricted form.

The correct solution is short in code, but rather tricky conceptually. It requires at least an intermediate level of understanding of the Drupal Form API and theming layer. The following steps can be used as a guideline:

  1. Choose a path for the new form. It is a logical choice to extend the default node edit path. That is, let /node/1234/edit refer to the default form and let /node/1234/edit/custom refer to your custom form. In your module, implement hook_menu() and copy the item from node.module which refers to /node/%node/edit. At this point you have an opportunity to turn it into a MENU_CALLBACK (if you don't want the new form to be accessible through the standard tabs) and also to restrict access by defining a new access callback. Don't forget to specify 'file path' => drupal_get_path('module', 'node'), otherwise the file node.pages.inc defined by node.module would not be found. The new item should look like so:
      $items['node/%node/edit/custom'] = array(
        'title' => 'Edit custom',
        'page callback' => 'node_page_edit',
        'page arguments' => array(1),
        'access callback' => 'yourmodule_edit_custom_access',
        'weight' => 1,
        'file' => 'node.pages.inc',
        'file path' => drupal_get_path('module', 'node'),
        'type' => MENU_CALLBACK,
      );
    
  2. Implement hook_form_alter() to disable access to unwanted fields on your custom form. You will also probably want to set a distinct theme function for the new form, so that you can arrange the wanted fields in a sensible way. The code in yourmodule_form_alter should look like so:
      if ($form_id == 'yourcontenttype_node_form' && arg(3) == 'custom') {
        $form['#theme'] = 'yourmodule_custom_form';
        $allowed_fields = array('field_first', 'field_second', 'field_third');
        foreach ($form as $key => &$field) {
          if (strpos($key, 'field_') === 0 && !in_array($key, $allowed_fields))
            $field['#access'] = FALSE;
        }
      }
    
    The original form might also contain other form elements that need to be disabled and whose names don't begin with field_. You can find out what they are visually by looking at the form and by dumping the keys of $form. One good example is the buttons key, which contains the submit, preview and delete buttons. It is possible that some form elements contributed by other modules are not yet present when your hook_form_alter() is called. In this case, you will likely have to implement another hook_form_alter() in another module and set this module's weight to a big value in the system table, to ensure that it executes last.
  3. Register the theme function yourcustom_form specified above via hook_theme() and also provide an actual implementation in a function called theme_yourmodule_custom_form, which receives $form as argument. Alternatively, you could declare it in hook_theme() as a template like so:
      'yourmodule_custom_form' => array(
        'arguments' => array('form' => NULL, 'user' => NULL),
        'template' => 'custom_form',
      ),
    
    and then create a template file custom_form.tpl.php in your theme folder.
  4. Within your theme function or template, render the whole form or individual fields like so:
      // Whole form
      // print drupal_render($form);
    
      // Individual fields
      print drupal_render($form['fieldgroup_tabs']['group_somegroup']);
      print drupal_render($form['buttons']['submit']);
    
    In general, if you cannot render the whole form at once because it would result in unwanted artifacts (e.g. unwanted group headers from the original form or some such), it is a good approach to render the wanted fields individually first and then call drupal_render($form) within a display:none div element, which will only output the as-of-yet unrendered fields. This will also ensure that any hidden fields are output at all. Mind the possible security implications; if you failed to set #access to FALSE on some fields in the step above, you'd get invisible, yet still updateable fields (the user might use Firebug to get at them). Accordingly, only rely on the display:none trick for visuals, but not as a method of concealing fields.
  5. In the problem statement a requirement to restrict allowed values for a particular field was mentioned. This can be done through the #afterbuild callback installed on the field in question, like so:
      // In hook_form_alter() if branch: 
      $form['field_first']['#after_build'][] =
         'yourmodule_field_first_after_build';
      // ...
    
      function yourmodule_field_first_after_build($element, &$form_state) {
        unset($element['value']['#options']['']);
        return $element;
      } 
    

A word of caution: if you are using a checkbox CCK field in your content type, you might to run into a bug when you set #access to FALSE, which would prevent the custom form from validating. As a workaround, two patches for optionwidgets.module were available at writing time (pick your favorite).

In general, when working with form data structures, remember that the content of these structures varies considerably depending on the current processing stage within the form engine. (Unfortunately, these variations and the separation between API and internals are rather badly documented, which makes it easy to write unstable code.) It's also good to realize that FAPI's "form elements" are hierarchical in nature and act as a mind-numbingly complicated powerful adapter layer between the database fields and HTML form inputs. In particular do realize that a single FAPI form element might expand into multiple form inputs and/or supply values for mutliple database fields.

For further information, I refer you to the following sources:

No comments:

Post a Comment