You are here

fieldgroup.module in Content Construction Kit (CCK) 6

Create field groups for CCK fields.

File

modules/fieldgroup/fieldgroup.module
View source
<?php

/**
 * @file
 * Create field groups for CCK fields.
 */

/**
 * Implementation of hook_init().
 */
function fieldgroup_init() {
  drupal_add_css(drupal_get_path('module', 'fieldgroup') . '/fieldgroup.css');
}

/**
 * Implementation of hook_menu().
 */
function fieldgroup_menu() {
  $items = array();

  // Make sure this doesn't fire until content_types is working,
  // needed to avoid errors on initial installation.
  if (!defined('MAINTENANCE_MODE')) {
    foreach (node_get_types() as $type) {
      $type_name = $type->type;
      $content_type = content_types($type_name);
      $type_url_str = $content_type['url_str'];
      $items['admin/content/node-type/' . $type_url_str . '/add_group'] = array(
        'title' => 'Add group',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'fieldgroup_edit_group_form',
          $content_type,
          '',
          'add',
        ),
        'access arguments' => array(
          'administer content types',
        ),
        'type' => MENU_LOCAL_TASK,
        'weight' => 3,
      );
      $items['admin/content/node-type/' . $type_url_str . '/groups/%/edit'] = array(
        'title' => 'Edit group',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'fieldgroup_edit_group_form',
          $content_type,
          5,
          'edit',
        ),
        'access arguments' => array(
          'administer content types',
        ),
        'type' => MENU_CALLBACK,
      );
      $items['admin/content/node-type/' . $type_url_str . '/groups/%/remove'] = array(
        'title' => 'Edit group',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
          'fieldgroup_remove_group',
          $content_type,
          5,
        ),
        'access arguments' => array(
          'administer content types',
        ),
        'type' => MENU_CALLBACK,
      );
    }
  }
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function fieldgroup_theme() {
  return array(
    'fieldgroup_simple' => array(
      'template' => 'fieldgroup',
      'arguments' => array(
        'element' => NULL,
      ),
    ),
    'fieldgroup_display_overview_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_elements().
 */
function fieldgroup_elements() {
  return array(
    'fieldgroup_simple' => array(),
  );
}

/**
 * Implementation of hook_fieldapi().
 */
function fieldgroup_content_fieldapi($op, $field) {
  switch ($op) {
    case 'delete':
      db_query("DELETE FROM {" . fieldgroup_fields_tablename() . " WHERE field_name = '%s'", $field['field_name']);
      break;
  }
  cache_clear_all('fieldgroup_data', content_cache_tablename());
}
function fieldgroup_edit_group_form(&$form_state, $content_type, $group_name, $action) {
  $groups = fieldgroup_groups($content_type['type']);
  if ($action == 'add') {

    //adding a new one
    $group = array();
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Add'),
      '#weight' => 10,
    );
  }
  elseif ($group = $groups[$group_name]) {
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Save'),
      '#weight' => 10,
    );
  }
  else {
    drupal_not_found();
    exit;
  }
  $form['label'] = array(
    '#type' => 'textfield',
    '#title' => t('Label'),
    '#default_value' => isset($group['label']) ? $group['label'] : '',
    '#required' => TRUE,
  );
  $form['settings']['#tree'] = TRUE;
  $form['settings']['form'] = array(
    '#type' => 'fieldset',
    '#title' => 'Form settings',
    '#description' => t('These settings apply to the group in the node editing form.'),
  );
  $form['settings']['form']['style'] = array(
    '#type' => 'radios',
    '#title' => t('Style'),
    '#default_value' => isset($group['settings']['form']['style']) ? $group['settings']['form']['style'] : 'fieldset',
    '#options' => array(
      'fieldset' => t('always open'),
      'fieldset_collapsible' => t('collapsible'),
      'fieldset_collapsed' => t('collapsed'),
    ),
  );
  $form['settings']['form']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Help text'),
    '#default_value' => isset($group['settings']['form']['description']) ? $group['settings']['form']['description'] : '',
    '#rows' => 5,
    '#description' => t('Instructions to present to the user on the editing form.'),
    '#required' => FALSE,
  );
  $form['settings']['display'] = array(
    '#type' => 'fieldset',
    '#title' => 'Display settings',
    '#description' => t('These settings apply to the group on node display.'),
  );
  $form['settings']['display']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => isset($group['settings']['display']['description']) ? $group['settings']['display']['description'] : '',
    '#rows' => 5,
    '#description' => t('A description of the group.'),
    '#required' => FALSE,
  );
  module_load_include('inc', 'content', 'includes/content.admin');
  foreach (array_merge(array_keys(_content_admin_display_contexts(CONTENT_CONTEXTS_SIMPLE)), array(
    'label',
  )) as $key) {
    $form['settings']['display'][$key] = array(
      '#type' => 'value',
      '#value' => isset($group['settings']['display'][$key]) ? $group['settings']['display'][$key] : 'fieldset',
    );
  }
  $form['weight'] = array(
    '#type' => 'hidden',
    '#default_value' => isset($group['weight']) ? $group['weight'] : 0,
  );
  $form['group_name'] = array(
    '#type' => 'hidden',
    '#default_value' => $group_name,
  );
  $form['#content_type'] = $content_type;
  $form['#group_action'] = $action;
  $form['#submit'][] = 'fieldgroup_edit_group_submit';
  return $form;
}

/**
 *  Group name validation for programmatic group addition.
 *
 * @todo
 * Come back here and do the same thing for groups as we've done for fields,
 * present a machine-readable name field as well as a label instead of
 * trying to create a machine-readable name from the label.
 */
function fieldgroup_edit_group_validate($form, &$form_state) {
  $form_values = $form_state['values'];
  $content_type = $form['#content_type'];
  $action = $form['#group_action'];
  if (!empty($form_values['group_name']) && $action == 'add') {
    $groups = fieldgroup_groups($content_type['type']);
    $group = $groups[$form_values['group_name']];
    if (isset($group[$form_values['group_name']])) {
      form_set_error('group_name', t('The group name %name already exists.', array(
        '%group_name' => $form_values['group_name'],
      )));
    }
    if (!preg_match('!^[a-z0-9_]+$!', $form_values['group_name'])) {
      form_set_error('group_name', t('The group name %name is invalid.', array(
        '%group_name' => $form_values['group_name'],
      )));
    }
  }
}
function fieldgroup_edit_group_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $content_type = $form['#content_type'];

  //  $groups = fieldgroup_groups($content_type['type']);
  //  $group = $groups[$form_values['group_name']];
  // TODO : when a group is added, it is displayed as a fieldset *with no label*
  // whereas the 'display field' overview states 'Label : above'
  // (the description is not displayed either)
  // fixed when the 'display field' form is submitted.
  fieldgroup_save_group($content_type['type'], $form_values);
  $form_state['redirect'] = 'admin/content/node-type/' . $content_type['url_str'] . '/fields';
}
function fieldgroup_remove_group(&$form_state, $content_type, $group_name) {
  $groups = fieldgroup_groups($content_type['type']);
  $group = isset($groups[$group_name]) ? $groups[$group_name] : '';
  if (empty($group)) {
    drupal_not_found();
    exit;
  }
  $form['#submit'][] = 'fieldgroup_remove_group_submit';
  $form['#content_type'] = $content_type;
  $form['#group_name'] = $group_name;
  return confirm_form($form, t('Are you sure you want to remove the group %label?', array(
    '%label' => t($group['label']),
  )), 'admin/content/node-type/' . $content_type['url_str'] . '/fields', t('This action cannot be undone.'), t('Remove'), t('Cancel'));
}
function fieldgroup_remove_group_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $content_type = $form['#content_type'];
  $group_name = $form['#group_name'];
  fieldgroup_delete($content_type['type'], $group_name);
  drupal_set_message(t('The group %group_name has been removed.', array(
    '%group_name' => $group_name,
  )));
  $form_state['redirect'] = 'admin/content/node-type/' . $content_type['url_str'] . '/fields';
}

/*
 * Returns all groups for a content type
 */
function fieldgroup_groups($content_type = '', $sorted = FALSE, $reset = FALSE) {
  static $groups, $groups_sorted;
  if (!isset($groups) || $reset) {
    if ($cached = cache_get('fieldgroup_data', content_cache_tablename())) {
      $data = $cached->data;
      $groups = $data['groups'];
      $groups_sorted = $data['groups_sorted'];
    }
    else {
      $result = db_query("SELECT * FROM {" . fieldgroup_tablename() . "} ORDER BY weight, group_name");
      $groups = array();
      $groups_sorted = array();
      while ($group = db_fetch_array($result)) {
        $group['settings'] = unserialize($group['settings']);
        $group['fields'] = array();
        $groups[$group['type_name']][$group['group_name']] = $group;
        $groups_sorted[$group['type_name']][] =& $groups[$group['type_name']][$group['group_name']];
      }

      //load fields
      $result = db_query("SELECT nfi.*, ng.group_name FROM {" . fieldgroup_tablename() . "} ng " . "INNER JOIN {" . fieldgroup_fields_tablename() . "} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name " . "INNER JOIN {" . content_instance_tablename() . "} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name " . "WHERE nfi.widget_active = 1 ORDER BY nfi.weight");
      while ($field = db_fetch_array($result)) {
        $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field;
      }
      cache_set('fieldgroup_data', array(
        'groups' => $groups,
        'groups_sorted' => $groups_sorted,
      ), content_cache_tablename());
    }
  }
  if (empty($content_type)) {
    return $groups;
  }
  elseif (empty($groups) || empty($groups[$content_type])) {
    return array();
  }
  return $sorted ? $groups_sorted[$content_type] : $groups[$content_type];
}
function _fieldgroup_groups_label($content_type) {
  $groups = fieldgroup_groups($content_type);
  $labels[''] = '<' . t('none') . '>';
  foreach ($groups as $group_name => $group) {
    $labels[$group_name] = t($group['label']);
  }
  return $labels;
}
function _fieldgroup_field_get_group($content_type, $field_name) {
  return db_result(db_query("SELECT group_name FROM {" . fieldgroup_fields_tablename() . "} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name));
}

/*
 * Implementation of hook_form_alter()
 */
function fieldgroup_form_alter(&$form, $form_state, $form_id) {
  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
    foreach (fieldgroup_groups($form['type']['#value']) as $group_name => $group) {
      $form[$group_name] = array(
        '#type' => 'fieldset',
        '#title' => t($group['label']),
        '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed',
        '#collapsible' => in_array($group['settings']['form']['style'], array(
          'fieldset_collapsed',
          'fieldset_collapsible',
        )),
        '#weight' => $group['weight'],
        '#description' => t($group['settings']['form']['description']),
        '#attributes' => array(
          'class' => strtr($group['group_name'], '_', '-'),
        ),
      );
      $has_accessible_field = FALSE;
      foreach ($group['fields'] as $field_name => $field) {
        if (isset($form[$field_name])) {
          $form[$group_name][$field_name] = $form[$field_name];

          // Track whether this group has any accessible fields within it.
          if ($form[$field_name]['#access'] !== FALSE) {
            $has_accessible_field = TRUE;
          }
          unset($form[$field_name]);
        }
      }
      if (!empty($group['fields']) && !element_children($form[$group_name])) {

        //hide the fieldgroup, because the fields are hidden too
        unset($form[$group_name]);
      }
      if (!$has_accessible_field) {

        // Hide the fieldgroup, because the fields are inaccessible.
        $form[$group_name]['#access'] = FALSE;
      }
    }
  }
  elseif ($form_id == '_content_admin_field') {
    $content_type = content_types($form['type_name']['#value']);
    $form['widget']['group'] = array(
      '#type' => 'select',
      '#title' => t('Display in group'),
      '#options' => _fieldgroup_groups_label($content_type['type']),
      '#default_value' => _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']),
      '#description' => t('Select a group, in which the field will be displayed on the editing form.'),
      '#weight' => 5,
    );
    $form['widget']['weight']['#weight'] = 5;
    $form['widget']['description']['#weight'] = 7;
    $form['#submit'][] = 'fieldgroup_content_admin_form_submit';
  }
  elseif ($form_id == 'content_admin_field_overview_form' && !empty($form['#groups'])) {
    $form['#submit'][] = 'fieldgroup_content_overview_form_submit';
  }
  elseif ($form_id == 'content_admin_display_overview_form' && !empty($form['#groups'])) {
    $form['#submit'][] = 'fieldgroup_display_overview_form_submit';
    if (!isset($form['submit'])) {
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Save'),
        '#weight' => 10,
      );
    }
  }
  elseif ($form_id == '_content_admin_field_remove') {
    $form['#submit'][] = 'fieldgroup_content_admin_field_remove_submit';
  }
}
function fieldgroup_content_admin_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  fieldgroup_update_fields($form_values);
}
function fieldgroup_content_overview_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $type_name = $form['#type_name'];
  foreach ($form_values as $key => $values) {

    // Update field parenting.
    if (in_array($key, $form['#fields'])) {

      // TODO : check the parent group does exist ?
      fieldgroup_update_fields(array(
        'field_name' => $key,
        'group' => $values['parent'],
        'type_name' => $type_name,
      ));
    }
    elseif (in_array($key, $form['#groups'])) {
      db_query("UPDATE {" . fieldgroup_tablename() . "} SET weight = %d WHERE type_name = '%s' AND group_name = '%s'", $values['weight'], $type_name, $key);
    }
  }
  cache_clear_all('fieldgroup_data', content_cache_tablename());
}
function fieldgroup_display_overview_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $groups = fieldgroup_groups($form['#type_name']);
  foreach ($form_values as $key => $values) {
    if (in_array($key, $form['#groups'])) {
      $group = $groups[$key];

      // We have numeric keys here, so we can't use array_merge.
      $group['settings']['display'] = $values + $group['settings']['display'];
      fieldgroup_save_group($form['#type_name'], $group);
    }
  }
}
function fieldgroup_content_admin_field_remove_submit($form, &$form_state) {
  $form_values = $form_state['values'];

  // TODO :
  // - when a (non last) field is removed from a group, a 'ghost row' remains in the fields overview
  // - when the last field is removed, the group disappears
  // seems to be fixed when emptying the cache.
  db_query("DELETE FROM {" . fieldgroup_fields_tablename() . "} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']);
}

/**
 * Implementation of hook_nodeapi().
 */
function fieldgroup_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      $context = $teaser ? 'teaser' : 'full';
      foreach (fieldgroup_groups($node->type) as $group_name => $group) {
        $label = $group['settings']['display']['label'] == 'above';
        $element = array(
          '#title' => $label ? t($group['label']) : '',
          '#description' => $label ? t($group['settings']['display']['description']) : '',
          '#weight' => $group['weight'],
        );
        switch ($group['settings']['display'][$context]['format']) {
          case 'simple':
            $element['#type'] = 'fieldgroup_simple';
            $element['#group_name'] = $group_name;
            break;
          case 'hidden':
            $element['#access'] = FALSE;
            break;
          case 'fieldset_collapsed':
            $element['#collapsed'] = TRUE;
          case 'fieldset_collapsible':
            $element['#collapsible'] = TRUE;
          case 'fieldset':
            $element['#type'] = 'fieldset';
            $element['#attributes'] = array(
              'class' => 'fieldgroup ' . strtr($group['group_name'], '_', '-'),
            );
            break;
        }
        $node->content[$group_name] = $element;
        foreach ($group['fields'] as $field_name => $field) {
          if (isset($node->content[$field_name])) {
            $node->content[$group_name][$field_name] = $node->content[$field_name];
            unset($node->content[$field_name]);
          }
        }
      }
      break;
    case 'alter':

      // Add back the formatted values in the 'view' element,
      // so that node templates can use it.
      $context = $teaser ? 'teaser' : 'full';
      foreach (fieldgroup_groups($node->type) as $group_name => $group) {
        foreach ($group['fields'] as $field_name => $field) {
          if (isset($node->content[$group_name][$field['field_name']])) {
            $element = $node->content[$group_name][$field['field_name']];
            if ($element['#single']) {

              // Single value formatter.
              foreach (element_children($element['items']) as $delta) {

                // Use isset() to avoid undefined index message on #children when field values are empty.
                $node->{$field_name}[$delta]['view'] = isset($element['items'][$delta]['#children']) ? $element['items'][$delta]['#children'] : '';
              }
            }
            else {

              // Multiple values formatter.
              $node->{$field_name}[0]['view'] = $element['items']['#children'];
            }
          }
          else {
            $node->{$field_name}[0]['view'] = '';
          }
        }
      }
      break;
  }
}

/**
 * Process variables for fieldgroup.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $group_name
 * - $group_name_css
 * - $label
 * - $description
 * - $content
 *
 * @see fieldgroup.tpl.php
 */
function template_preprocess_fieldgroup_simple(&$variables) {
  $element = $variables['element'];
  $variables['group_name'] = $element['#group_name'];
  $variables['group_name_css'] = strtr($element['#group_name'], '_', '-');
  $variables['label'] = isset($element['#title']) ? $element['#title'] : '';
  $variables['description'] = isset($element['#description']) ? $element['#description'] : '';
  $variables['content'] = isset($element['#children']) ? $element['#children'] : '';
}

/*
 * Get the group name for a field.
 * If the field isn't in a group, FALSE will be returned.
 * @return The name of the group, or FALSE.
 */
function fieldgroup_get_group($content_type, $field_name) {
  foreach (fieldgroup_groups($content_type) as $group_name => $group) {
    if (in_array($field_name, array_keys($group['fields']))) {
      return $group_name;
    }
  }
  return FALSE;
}

/**
 *  Implementation of hook_node_type()
 *  React to change in node types
 */
function fieldgroup_node_type($op, $info) {
  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {

    // update the tables
    db_query("UPDATE {" . fieldgroup_tablename() . "} SET type_name='%s' WHERE type_name='%s'", array(
      $info->type,
      $info->old_type,
    ));
    db_query("UPDATE {" . fieldgroup_fields_tablename() . "} SET type_name='%s' WHERE type_name='%s'", array(
      $info->type,
      $info->old_type,
    ));
    cache_clear_all('fieldgroup_data', content_cache_tablename());
  }
  elseif ($op == 'delete') {
    db_query("DELETE FROM {" . fieldgroup_tablename() . "} WHERE type_name = '%s'", $info->type);
    db_query("DELETE FROM {" . fieldgroup_fields_tablename() . "} WHERE type_name = '%s'", $info->type);
  }
}
function fieldgroup_tablename($version = NULL) {
  if (is_null($version)) {
    $version = variable_get('fieldgroup_schema_version', 0);
  }
  return $version < 6000 ? 'node_group' : 'content_group';
}
function fieldgroup_fields_tablename($version = NULL) {
  if (is_null($version)) {
    $version = variable_get('fieldgroup_schema_version', 0);
  }
  return $version < 6000 ? 'node_group_fields' : 'content_group_fields';
}

/**
 * CRUD API for fieldgroup module.
 *
 * @todo
 * Make this into more of a real API for groups.
 */

/*
 * Saves the given group for this content-type
 */
function fieldgroup_save_group($type_name, $group) {
  $groups = fieldgroup_groups($type_name);
  if (!isset($groups[$group['group_name']])) {

    // Accept group name from programmed submissions if valid.
    if (!empty($group['group_name'])) {
      $group_name = $group['group_name'];
    }
    else {

      // Otherwise, find a valid, computer-friendly name.
      $group_name = trim($group['label']);
      $group_name = drupal_strtolower($group_name);
      $group_name = str_replace(array(
        ' ',
        '-',
      ), '_', $group_name);
      $group_name = preg_replace('/[^a-z0-9_]/', '', $group_name);
      $group_name = 'group_' . $group_name;
      $group_name = substr($group_name, 0, 30);
      if (isset($groups[$group_name])) {
        $group_name_base = $group_name;
        $counter = 0;
        while (isset($groups[$group_name])) {
          $group_name = $group_name_base . '_' . $counter++;
        }
      }
    }
    db_query("INSERT INTO {" . fieldgroup_tablename() . "} (type_name, group_name, label, settings, weight)\n             VALUES ('%s', '%s', '%s', '%s', %d)", $type_name, $group_name, $group['label'], serialize($group['settings']), $group['weight']);
    cache_clear_all('fieldgroup_data', content_cache_tablename());
    return SAVED_NEW;
  }
  else {
    db_query("UPDATE {" . fieldgroup_tablename() . "} SET label = '%s', settings = '%s', weight = %d " . "WHERE type_name = '%s' AND group_name = '%s'", $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']);
    cache_clear_all('fieldgroup_data', content_cache_tablename());
    return SAVED_UPDATED;
  }
}
function fieldgroup_update_fields($form_values) {
  $default = _fieldgroup_field_get_group($form_values['type_name'], $form_values['field_name']);
  if ($default != $form_values['group']) {
    if ($form_values['group'] && !$default) {
      db_query("INSERT INTO {" . fieldgroup_fields_tablename() . "} (type_name, group_name, field_name) VALUES ('%s', '%s', '%s')", $form_values['type_name'], $form_values['group'], $form_values['field_name']);
    }
    elseif ($form_values['group']) {
      db_query("UPDATE {" . fieldgroup_fields_tablename() . "} SET group_name = '%s' WHERE type_name = '%s' AND field_name = '%s'", $form_values['group'], $form_values['type_name'], $form_values['field_name']);
    }
    else {
      db_query("DELETE FROM {" . fieldgroup_fields_tablename() . "} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']);
    }
    cache_clear_all('fieldgroup_data', content_cache_tablename());
  }
}
function fieldgroup_delete($content_type, $group_name) {
  db_query("DELETE FROM {" . fieldgroup_tablename() . "} WHERE  type_name = '%s' AND group_name = '%s'", $content_type, $group_name);
  db_query("DELETE FROM {" . fieldgroup_fields_tablename() . "} WHERE  type_name = '%s' AND group_name = '%s'", $content_type, $group_name);
  cache_clear_all('fieldgroup_data', content_cache_tablename());
}